Files

714 lines
26 KiB
Swift

import XCTest
import GRDB
private class Person : Record {
var id: Int64!
var name: String!
var age: Int?
var creationDate: Date!
init(id: Int64? = nil, name: String? = nil, age: Int? = nil, creationDate: Date? = nil) {
self.id = id
self.name = name
self.age = age
self.creationDate = creationDate
super.init()
}
static func setup(inDatabase db: Database) throws {
try db.execute(sql: """
CREATE TABLE persons (
id INTEGER PRIMARY KEY,
creationDate TEXT NOT NULL,
name TEXT NOT NULL,
age INT)
""")
}
// Record
override class var databaseTableName: String {
"persons"
}
required init(row: Row) throws {
id = row["id"]
age = row["age"]
name = row["name"]
creationDate = row["creationDate"]
try super.init(row: row)
}
override func encode(to container: inout PersistenceContainer) throws {
container["id"] = id
container["name"] = name
container["age"] = age
container["creationDate"] = creationDate
}
override func willInsert(_ db: Database) throws {
if creationDate == nil {
creationDate = Date()
}
try super.willInsert(db)
}
override func didInsert(_ inserted: InsertionSuccess) {
super.didInsert(inserted)
id = inserted.rowID
}
}
private class IntegerPropertyOnRealAffinityColumn : Record {
var value: Int?
init(value: Int?) {
self.value = value
super.init()
}
// Record
required init(row: Row) throws {
value = row["value"]
try super.init(row: row)
}
override func encode(to container: inout PersistenceContainer) throws {
container["value"] = value
}
}
private class PersonWithModifiedCaseColumns: Record {
var id: Int64!
var name: String!
var age: Int?
var creationDate: Date!
init(id: Int64? = nil, name: String? = nil, age: Int? = nil, creationDate: Date? = nil) {
self.id = id
self.name = name
self.age = age
self.creationDate = creationDate
super.init()
}
// Record
override class var databaseTableName: String {
"persons"
}
required init(row: Row) throws {
id = row["ID"]
age = row["AGE"]
name = row["NAME"]
creationDate = row["CREATIONDATE"]
try super.init(row: row)
}
override func encode(to container: inout PersistenceContainer) throws {
container["ID"] = id
container["NAME"] = name
container["AGE"] = age
container["CREATIONDATE"] = creationDate
}
override func willInsert(_ db: Database) throws {
if creationDate == nil {
creationDate = Date()
}
try super.willInsert(db)
}
override func didInsert(_ inserted: InsertionSuccess) {
super.didInsert(inserted)
id = inserted.rowID
}
}
class RecordEditedTests: GRDBTestCase {
override func setup(_ dbWriter: some DatabaseWriter) throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("createPerson", migrate: Person.setup)
try migrator.migrate(dbWriter)
}
func testRecordIsEditedAfterInit() {
// Create a Record. No fetch has happen, so we don't know if it is
// identical to its eventual row in the database. So it is edited.
let person = Person(name: "Arthur", age: 41)
XCTAssertTrue(person.hasDatabaseChanges)
}
func testRecordIsEditedAfterInitFromRow() throws {
// Create a Record from a row. The row may not come from the database.
// So it is edited.
let row = Row(["name": "Arthur", "age": 41])
let person = try Person(row: row)
XCTAssertTrue(person.hasDatabaseChanges)
}
func testRecordIsNotEditedAfterFullFetch() throws {
// Fetch a record from a row that contains all the columns in
// persistence container: An update statement, which only saves the
// columns in persistence container would perform no change. So the
// record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try Person(name: "Arthur", age: 41).insert(db)
let person = try Person.fetchOne(db, sql: "SELECT * FROM persons")!
XCTAssertFalse(person.hasDatabaseChanges)
}
try dbQueue.inDatabase { db in
try PersonWithModifiedCaseColumns(name: "Arthur", age: 41).insert(db)
let person = try PersonWithModifiedCaseColumns.fetchOne(db, sql: "SELECT * FROM persons")!
XCTAssertFalse(person.hasDatabaseChanges)
}
}
func testRecordIsNotEditedAfterFullFetchWithIntegerPropertyOnRealAffinityColumn() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE t (value REAL)")
try db.execute(sql: "INSERT INTO t (value) VALUES (1)")
let record = try IntegerPropertyOnRealAffinityColumn.fetchOne(db, sql: "SELECT * FROM t")!
XCTAssertFalse(record.hasDatabaseChanges)
}
}
func testRecordIsNotEditedAfterWiderThanFullFetch() throws {
// Fetch a record from a row that contains all the columns in
// persistence container, plus extra ones: An update statement,
// which only saves the columns in persistence container would
// perform no change. So the record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try Person(name: "Arthur", age: 41).insert(db)
let person = try Person.fetchOne(db, sql: "SELECT *, 1 AS foo FROM persons")!
XCTAssertFalse(person.hasDatabaseChanges)
}
try dbQueue.inDatabase { db in
try PersonWithModifiedCaseColumns(name: "Arthur", age: 41).insert(db)
let person = try PersonWithModifiedCaseColumns.fetchOne(db, sql: "SELECT *, 1 AS foo FROM persons")!
XCTAssertFalse(person.hasDatabaseChanges)
}
}
func testRecordIsEditedAfterPartialFetch() throws {
// Fetch a record from a row that does not contain all the columns in
// persistence container: An update statement saves the columns in
// persistence container, so it may perform unpredictable change.
// So the record is edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try Person(name: "Arthur", age: 41).insert(db)
let person = try Person.fetchOne(db, sql: "SELECT name FROM persons")!
XCTAssertTrue(person.hasDatabaseChanges)
}
try dbQueue.inDatabase { db in
try PersonWithModifiedCaseColumns(name: "Arthur", age: 41).insert(db)
let person = try PersonWithModifiedCaseColumns.fetchOne(db, sql: "SELECT name FROM persons")!
XCTAssertTrue(person.hasDatabaseChanges)
}
}
func testRecordIsNotEditedAfterInsert() throws {
// After insertion, a record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: 41)
try person.insert(db)
XCTAssertFalse(person.hasDatabaseChanges)
}
try dbQueue.inDatabase { db in
let person = PersonWithModifiedCaseColumns(name: "Arthur", age: 41)
try person.insert(db)
XCTAssertFalse(person.hasDatabaseChanges)
}
}
func testRecordIsEditedAfterValueChange() throws {
// Any change in a value exposed in persistence container yields a
// record that is edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
do {
let person = Person(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.name != nil)
person.name = "Bobby" // non-nil vs. non-nil
XCTAssertTrue(person.hasDatabaseChanges)
}
do {
let person = Person(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.name != nil)
person.name = nil // non-nil vs. nil
XCTAssertTrue(person.hasDatabaseChanges)
}
do {
let person = Person(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.age == nil)
person.age = 41 // nil vs. non-nil
XCTAssertTrue(person.hasDatabaseChanges)
}
}
try dbQueue.inDatabase { db in
do {
let person = PersonWithModifiedCaseColumns(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.name != nil)
person.name = "Bobby" // non-nil vs. non-nil
XCTAssertTrue(person.hasDatabaseChanges)
}
do {
let person = PersonWithModifiedCaseColumns(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.name != nil)
person.name = nil // non-nil vs. nil
XCTAssertTrue(person.hasDatabaseChanges)
}
do {
let person = PersonWithModifiedCaseColumns(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.age == nil)
person.age = 41 // nil vs. non-nil
XCTAssertTrue(person.hasDatabaseChanges)
}
}
}
func testRecordIsNotEditedAfterSameValueChange() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
do {
let person = Person(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.name != nil)
person.name = "Arthur" // non-nil vs. non-nil
XCTAssertFalse(person.hasDatabaseChanges)
}
do {
let person = Person(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.age == nil)
person.age = nil // nil vs. nil
XCTAssertFalse(person.hasDatabaseChanges)
}
}
try dbQueue.inDatabase { db in
do {
let person = PersonWithModifiedCaseColumns(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.name != nil)
person.name = "Arthur" // non-nil vs. non-nil
XCTAssertFalse(person.hasDatabaseChanges)
}
do {
let person = PersonWithModifiedCaseColumns(name: "Arthur")
try person.insert(db)
XCTAssertTrue(person.age == nil)
person.age = nil // nil vs. nil
XCTAssertFalse(person.hasDatabaseChanges)
}
}
}
func testRecordIsNotEditedAfterUpdate() throws {
// After update, a record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: 41)
try person.insert(db)
person.name = "Bobby"
try person.update(db)
XCTAssertFalse(person.hasDatabaseChanges)
}
try dbQueue.inDatabase { db in
let person = PersonWithModifiedCaseColumns(name: "Arthur", age: 41)
try person.insert(db)
person.name = "Bobby"
try person.update(db)
XCTAssertFalse(person.hasDatabaseChanges)
}
}
func testRecordIsNotEditedAfterSave() throws {
// After save, a record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: 41)
try person.save(db)
XCTAssertFalse(person.hasDatabaseChanges)
person.name = "Bobby"
XCTAssertTrue(person.hasDatabaseChanges)
try person.save(db)
XCTAssertFalse(person.hasDatabaseChanges)
}
try dbQueue.inDatabase { db in
let person = PersonWithModifiedCaseColumns(name: "Arthur", age: 41)
try person.save(db)
XCTAssertFalse(person.hasDatabaseChanges)
person.name = "Bobby"
XCTAssertTrue(person.hasDatabaseChanges)
try person.save(db)
XCTAssertFalse(person.hasDatabaseChanges)
}
}
func testRecordIsEditedAfterPrimaryKeyChange() throws {
// After reload, a record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: 41)
try person.insert(db)
person.id = person.id + 1
XCTAssertTrue(person.hasDatabaseChanges)
}
try dbQueue.inDatabase { db in
let person = PersonWithModifiedCaseColumns(name: "Arthur", age: 41)
try person.insert(db)
person.id = person.id + 1
XCTAssertTrue(person.hasDatabaseChanges)
}
}
func testChangesAfterInit() throws {
let person = Person(name: "Arthur", age: 41)
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 4)
for (column, old) in changes {
switch column {
case "id":
XCTAssertTrue(old == nil)
case "name":
XCTAssertTrue(old == nil)
case "age":
XCTAssertTrue(old == nil)
case "creationDate":
XCTAssertTrue(old == nil)
default:
XCTFail("Unexpected column: \(column)")
}
}
}
func testChangesAfterInitFromRow() throws {
let person = try Person(row: Row(["name": "Arthur", "age": 41]))
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 4)
for (column, old) in changes {
switch column {
case "id":
XCTAssertTrue(old == nil)
case "name":
XCTAssertTrue(old == nil)
case "age":
XCTAssertTrue(old == nil)
case "creationDate":
XCTAssertTrue(old == nil)
default:
XCTFail("Unexpected column: \(column)")
}
}
}
func testChangesAfterFullFetch() throws {
// Fetch a record from a row that contains all the columns in
// persistence container: An update statement, which only saves the
// columns in persistence container would perform no change. So the
// record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try Person(name: "Arthur", age: 41).insert(db)
do {
let person = try Person.fetchOne(db, sql: "SELECT * FROM persons")!
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 0)
}
do {
let persons = try Person.fetchAll(db, sql: "SELECT * FROM persons")
let changes = try persons[0].databaseChanges
XCTAssertEqual(changes.count, 0)
}
do {
let persons = try Person.fetchCursor(db, sql: "SELECT * FROM persons")
let changes = try persons.next()!.databaseChanges
XCTAssertEqual(changes.count, 0)
}
do {
let person = try Person.fetchOne(db, sql: "SELECT * FROM persons", adapter: SuffixRowAdapter(fromIndex: 0))!
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 0)
}
}
try dbQueue.inDatabase { db in
try PersonWithModifiedCaseColumns(name: "Arthur", age: 41).insert(db)
let person = try PersonWithModifiedCaseColumns.fetchOne(db, sql: "SELECT * FROM persons")!
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 0)
}
}
func testChangesAfterPartialFetch() throws {
// Fetch a record from a row that does not contain all the columns in
// persistence container: An update statement saves the columns in
// persistence container, so it may perform unpredictable change.
// So the record is edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try Person(name: "Arthur", age: 41).insert(db)
let person = try Person.fetchOne(db, sql: "SELECT name FROM persons")!
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 3)
for (column, old) in changes {
switch column {
case "id":
XCTAssertTrue(old == nil)
case "age":
XCTAssertTrue(old == nil)
case "creationDate":
XCTAssertTrue(old == nil)
default:
XCTFail("Unexpected column: \(column)")
}
}
}
try dbQueue.inDatabase { db in
try PersonWithModifiedCaseColumns(name: "Arthur", age: 41).insert(db)
let person = try PersonWithModifiedCaseColumns.fetchOne(db, sql: "SELECT name FROM persons")!
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 3)
for (column, old) in changes {
switch column {
case "ID":
XCTAssertTrue(old == nil)
case "AGE":
XCTAssertTrue(old == nil)
case "CREATIONDATE":
XCTAssertTrue(old == nil)
default:
XCTFail("Unexpected column: \(column)")
}
}
}
}
func testChangesAfterInsert() throws {
// After insertion, a record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: 41)
try person.insert(db)
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 0)
}
try dbQueue.inDatabase { db in
let person = PersonWithModifiedCaseColumns(name: "Arthur", age: 41)
try person.insert(db)
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 0)
}
}
func testChangesAfterValueChange() throws {
// Any change in a value exposed in persistence container yields a
// record that is edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: nil)
try person.insert(db)
person.name = "Bobby" // non-nil -> non-nil
person.age = 41 // nil -> non-nil
person.creationDate = nil // non-nil -> nil
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 3)
for (column, old) in changes {
switch column {
case "name":
XCTAssertEqual(old, "Arthur".databaseValue)
case "age":
XCTAssertEqual(old, DatabaseValue.null)
case "creationDate":
XCTAssertTrue(Date.fromDatabaseValue(old!) != nil)
default:
XCTFail("Unexpected column: \(column)")
}
}
}
try dbQueue.inDatabase { db in
let person = PersonWithModifiedCaseColumns(name: "Arthur", age: nil)
try person.insert(db)
person.name = "Bobby" // non-nil -> non-nil
person.age = 41 // nil -> non-nil
person.creationDate = nil // non-nil -> nil
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 3)
for (column, old) in changes {
switch column {
case "NAME":
XCTAssertEqual(old, "Arthur".databaseValue)
case "AGE":
XCTAssertEqual(old, DatabaseValue.null)
case "CREATIONDATE":
XCTAssertTrue(Date.fromDatabaseValue(old!) != nil)
default:
XCTFail("Unexpected column: \(column)")
}
}
}
}
func testChangesAfterUpdate() throws {
// After update, a record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: 41)
try person.insert(db)
person.name = "Bobby"
try person.update(db)
XCTAssertFalse(person.hasDatabaseChanges)
try XCTAssert(person.databaseChanges.isEmpty)
}
try dbQueue.inDatabase { db in
let person = PersonWithModifiedCaseColumns(name: "Arthur", age: 41)
try person.insert(db)
person.name = "Bobby"
try person.update(db)
XCTAssertFalse(person.hasDatabaseChanges)
try XCTAssert(person.databaseChanges.isEmpty)
}
}
func testChangesAfterSave() throws {
// After save, a record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: 41)
try person.save(db)
XCTAssertFalse(person.hasDatabaseChanges)
try XCTAssert(person.databaseChanges.isEmpty)
person.name = "Bobby"
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 1)
for (column, old) in changes {
switch column {
case "name":
XCTAssertEqual(old, "Arthur".databaseValue)
default:
XCTFail("Unexpected column: \(column)")
}
}
try person.save(db)
XCTAssertFalse(person.hasDatabaseChanges)
try XCTAssert(person.databaseChanges.isEmpty)
}
try dbQueue.inDatabase { db in
let person = PersonWithModifiedCaseColumns(name: "Arthur", age: 41)
try person.save(db)
XCTAssertFalse(person.hasDatabaseChanges)
try XCTAssert(person.databaseChanges.isEmpty)
person.name = "Bobby"
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 1)
for (column, old) in changes {
switch column {
case "NAME":
XCTAssertEqual(old, "Arthur".databaseValue)
default:
XCTFail("Unexpected column: \(column)")
}
}
try person.save(db)
XCTAssertFalse(person.hasDatabaseChanges)
try XCTAssert(person.databaseChanges.isEmpty)
}
}
func testChangesAfterPrimaryKeyChange() throws {
// After reload, a record is not edited.
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: 41)
try person.insert(db)
person.id = person.id + 1
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 1)
for (column, old) in changes {
switch column {
case "id":
XCTAssertEqual(old, (person.id - 1).databaseValue)
default:
XCTFail("Unexpected column: \(column)")
}
}
}
try dbQueue.inDatabase { db in
let person = PersonWithModifiedCaseColumns(name: "Arthur", age: 41)
try person.insert(db)
person.id = person.id + 1
let changes = try person.databaseChanges
XCTAssertEqual(changes.count, 1)
for (column, old) in changes {
switch column {
case "ID":
XCTAssertEqual(old, (person.id - 1).databaseValue)
default:
XCTFail("Unexpected column: \(column)")
}
}
}
}
func testUpdateChanges() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let person = Person(name: "Arthur", age: 41)
do {
XCTAssertTrue(person.hasDatabaseChanges)
try person.updateChanges(db)
XCTFail("Expected RecordError")
} catch RecordError.recordNotFound(databaseTableName: "persons", key: ["id": .null]) { }
try person.insert(db)
// Nothing to update
let initialChangesCount = db.totalChangesCount
XCTAssertFalse(person.hasDatabaseChanges)
try XCTAssertFalse(person.updateChanges(db))
XCTAssertEqual(db.totalChangesCount, initialChangesCount)
// Nothing to update
person.age = 41
XCTAssertFalse(person.hasDatabaseChanges)
try XCTAssertFalse(person.updateChanges(db))
XCTAssertEqual(db.totalChangesCount, initialChangesCount)
// Update single column
person.age = 42
try XCTAssertEqual(Set(person.databaseChanges.keys), ["age"])
try XCTAssertTrue(person.updateChanges(db))
XCTAssertEqual(db.totalChangesCount, initialChangesCount + 1)
XCTAssertEqual(lastSQLQuery, "UPDATE \"persons\" SET \"age\"=42 WHERE \"id\"=1")
// Update two columns
person.name = "Barbara"
person.age = 43
try XCTAssertEqual(Set(person.databaseChanges.keys), ["age", "name"])
try XCTAssertTrue(person.updateChanges(db))
XCTAssertEqual(db.totalChangesCount, initialChangesCount + 2)
let fetchedPerson = try Person.fetchOne(db, key: person.id)
XCTAssertEqual(fetchedPerson?.name, person.name)
XCTAssertEqual(fetchedPerson?.age, person.age)
}
}
}