import XCTest import GRDB private struct Player: FetchableRecord, MutablePersistableRecord, Codable { static let databaseTableName = "players" var id: Int64? var name: String? var score: Int? var creationDate: Date? init(id: Int64? = nil, name: String? = nil, score: Int? = nil, creationDate: Date? = nil) { self.id = id self.name = name self.score = score self.creationDate = creationDate } mutating func willInsert(_ db: Database) throws { if creationDate == nil { creationDate = Date() } } mutating func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID } } class MutablePersistableRecordChangesTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { try dbWriter.write { db in try db.create(table: "players") { t in t.primaryKey("id", .integer) t.column("name", .text) t.column("score", .integer) t.column("creationDate", .datetime) } } } func testDegenerateDatabaseEqualsWithSelf() throws { struct DegenerateRecord: MutablePersistableRecord { static let databaseTableName = "degenerated" func encode(to container: inout PersistenceContainer) { } } let record = DegenerateRecord() XCTAssertTrue(record.databaseEquals(record)) try XCTAssertTrue(record.databaseChanges(from: record).isEmpty) let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in // table must exist try db.create(table: DegenerateRecord.databaseTableName) { t in t.autoIncrementedPrimaryKey("id") } let totalChangesCount = db.totalChangesCount try XCTAssertFalse(record.updateChanges(db, from: record)) XCTAssertEqual(db.totalChangesCount, totalChangesCount) } } func testDatabaseEqualsWithSelf() throws { do { let player = Player(id: nil, name: nil, score: nil, creationDate: nil) XCTAssertTrue(player.databaseEquals(player)) try XCTAssertTrue(player.databaseChanges(from: player).isEmpty) let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let totalChangesCount = db.totalChangesCount try XCTAssertFalse(player.updateChanges(db, from: player)) XCTAssertEqual(db.totalChangesCount, totalChangesCount) } } do { let player = Player(id: 1, name: "foo", score: 42, creationDate: Date()) XCTAssertTrue(player.databaseEquals(player)) try XCTAssertTrue(player.databaseChanges(from: player).isEmpty) let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let totalChangesCount = db.totalChangesCount try XCTAssertFalse(player.updateChanges(db, from: player)) XCTAssertEqual(db.totalChangesCount, totalChangesCount) } } } func testRecordValueChange() throws { let dbQueue = try makeDatabaseQueue() var player = Player(id: 1, name: "Arthur", score: nil, creationDate: nil) try dbQueue.inDatabase { db in try player.insert(db) } do { // non-nil vs. non-nil var newPlayer = player newPlayer.name = "Bobby" XCTAssertFalse(newPlayer.databaseEquals(player)) let changes = try newPlayer.databaseChanges(from: player) XCTAssertEqual(changes.count, 1) XCTAssertEqual(changes["name"]!, "Arthur".databaseValue) try dbQueue.inDatabase { db in let totalChangesCount = db.totalChangesCount try XCTAssertTrue(newPlayer.updateChanges(db, from: player)) XCTAssertEqual(db.totalChangesCount, totalChangesCount + 1) XCTAssertEqual(lastSQLQuery, "UPDATE \"players\" SET \"name\"=\'Bobby\' WHERE \"id\"=1") } } do { // non-nil vs. nil var newPlayer = player newPlayer.name = nil XCTAssertFalse(newPlayer.databaseEquals(player)) let changes = try newPlayer.databaseChanges(from: player) XCTAssertEqual(changes.count, 1) XCTAssertEqual(changes["name"]!, "Arthur".databaseValue) try dbQueue.inDatabase { db in let totalChangesCount = db.totalChangesCount try XCTAssertTrue(newPlayer.updateChanges(db, from: player)) XCTAssertEqual(db.totalChangesCount, totalChangesCount + 1) XCTAssertEqual(lastSQLQuery, "UPDATE \"players\" SET \"name\"=NULL WHERE \"id\"=1") } } do { // nil vs. non-nil var newPlayer = player newPlayer.score = 41 XCTAssertFalse(newPlayer.databaseEquals(player)) let changes = try newPlayer.databaseChanges(from: player) XCTAssertEqual(changes.count, 1) XCTAssertEqual(changes["score"]!, .null) try dbQueue.inDatabase { db in let totalChangesCount = db.totalChangesCount try XCTAssertTrue(newPlayer.updateChanges(db, from: player)) XCTAssertEqual(db.totalChangesCount, totalChangesCount + 1) XCTAssertEqual(lastSQLQuery, "UPDATE \"players\" SET \"score\"=41 WHERE \"id\"=1") } } do { // multiple changes var newPlayer = player newPlayer.name = "Bobby" newPlayer.score = 41 XCTAssertFalse(newPlayer.databaseEquals(player)) let changes = try newPlayer.databaseChanges(from: player) XCTAssertEqual(changes.count, 2) XCTAssertEqual(changes["name"]!, "Arthur".databaseValue) XCTAssertEqual(changes["score"]!, .null) try dbQueue.inDatabase { db in let totalChangesCount = db.totalChangesCount try XCTAssertTrue(newPlayer.updateChanges(db, from: player)) XCTAssertEqual(db.totalChangesCount, totalChangesCount + 1) let fetchedPlayer = try Player.fetchOne(db, key: player.id) XCTAssertEqual(fetchedPlayer?.name, newPlayer.name) XCTAssertEqual(fetchedPlayer?.score, newPlayer.score) } } } func testDatabaseEqualsWithDifferentTypesAndDifferentWidth() throws { // Mangle column case as well, for fun ;-) struct NarrowPlayer: MutablePersistableRecord, Codable { static let databaseTableName = "players" var ID: Int64? var NAME: String? } let dbQueue = try makeDatabaseQueue() do { var oldPlayer = NarrowPlayer(ID: 1, NAME: "Arthur") let newPlayer = Player(id: 1, name: "Arthur", score: 41, creationDate: nil) let changes = try newPlayer.databaseChanges(from: oldPlayer) XCTAssertEqual(changes.count, 1) XCTAssertEqual(changes["score"]!, .null) try dbQueue.inTransaction { db in try oldPlayer.insert(db) let totalChangesCount = db.totalChangesCount try XCTAssertTrue(newPlayer.updateChanges(db, from: oldPlayer)) XCTAssertEqual(db.totalChangesCount, totalChangesCount + 1) XCTAssertEqual(lastSQLQuery, "UPDATE \"players\" SET \"score\"=41 WHERE \"id\"=1") return .rollback } } do { var oldPlayer = NarrowPlayer(ID: 1, NAME: "Bobby") let newPlayer = Player(id: 1, name: "Arthur", score: 42, creationDate: nil) let changes = try newPlayer.databaseChanges(from: oldPlayer) XCTAssertEqual(changes.count, 2) XCTAssertEqual(changes["name"]!, "Bobby".databaseValue) XCTAssertEqual(changes["score"]!, .null) try dbQueue.inTransaction { db in try oldPlayer.insert(db) let totalChangesCount = db.totalChangesCount try XCTAssertTrue(newPlayer.updateChanges(db, from: oldPlayer)) XCTAssertEqual(db.totalChangesCount, totalChangesCount + 1) let fetchedPlayer = try Player.fetchOne(db, key: newPlayer.id) XCTAssertEqual(fetchedPlayer?.name, newPlayer.name) XCTAssertEqual(fetchedPlayer?.score, newPlayer.score) return .rollback } } do { let oldPlayer = Player(id: 1, name: "Arthur", score: 42, creationDate: nil) let newPlayer = NarrowPlayer(ID: 1, NAME: "Arthur") let changes = try newPlayer.databaseChanges(from: oldPlayer) XCTAssertTrue(changes.isEmpty) try dbQueue.inTransaction { db in let totalChangesCount = db.totalChangesCount try XCTAssertFalse(newPlayer.updateChanges(db, from: oldPlayer)) XCTAssertEqual(db.totalChangesCount, totalChangesCount) return .rollback } } do { let oldPlayer = Player(id: 1, name: "Arthur", score: nil, creationDate: nil) let newPlayer = NarrowPlayer(ID: 1, NAME: "Arthur") let changes = try newPlayer.databaseChanges(from: oldPlayer) XCTAssertTrue(changes.isEmpty) try dbQueue.inTransaction { db in let totalChangesCount = db.totalChangesCount try XCTAssertFalse(newPlayer.updateChanges(db, from: oldPlayer)) XCTAssertEqual(db.totalChangesCount, totalChangesCount) return .rollback } } do { var oldPlayer = Player(id: 1, name: "Arthur", score: nil, creationDate: nil) let newPlayer = NarrowPlayer(ID: 1, NAME: "Bobby") let changes = try newPlayer.databaseChanges(from: oldPlayer) XCTAssertEqual(changes.count, 1) XCTAssertEqual(changes["NAME"]!, "Arthur".databaseValue) try dbQueue.inTransaction { db in try oldPlayer.insert(db) let totalChangesCount = db.totalChangesCount try XCTAssertTrue(newPlayer.updateChanges(db, from: oldPlayer)) XCTAssertEqual(db.totalChangesCount, totalChangesCount + 1) XCTAssertEqual(lastSQLQuery, "UPDATE \"players\" SET \"name\"='Bobby' WHERE \"id\"=1") return .rollback } } } func testUpdateChangesWithRecord() throws { class MyRecord: Record { var id: Int64? var firstName: String? var lastName: String? init(id: Int64?, firstName: String?, lastName: String?) { self.id = id self.firstName = firstName self.lastName = lastName super.init() } override class var databaseTableName: String { "myRecord" } enum Columns: String, ColumnExpression { case id, firstName, lastName } required init(row: Row) throws { id = row[Columns.id] firstName = row[Columns.firstName] lastName = row[Columns.lastName] try super.init(row: row) } override func encode(to container: inout PersistenceContainer) throws { container[Columns.id] = id container[Columns.firstName] = firstName container[Columns.lastName] = lastName } override func didInsert(_ inserted: InsertionSuccess) { super.didInsert(inserted) id = inserted.rowID } } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "myRecord") { t in t.autoIncrementedPrimaryKey("id") t.column("firstName", .text) t.column("lastName", .text) } var record = MyRecord(id: nil, firstName: "Arthur", lastName: "Smith") try record.insert(db) do { sqlQueries = [] let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = nil } XCTAssertEqual(record.firstName, nil) XCTAssertEqual(record.lastName, "Smith") XCTAssertTrue(modified) XCTAssertEqual(lastSQLQuery, "UPDATE \"myRecord\" SET \"firstName\"=NULL WHERE \"id\"=1") } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" } XCTAssertEqual(record.firstName, "Bob") XCTAssertEqual(record.lastName, "Johnson") XCTAssertTrue(modified) XCTAssertTrue([ "UPDATE \"myRecord\" SET \"firstName\"=\'Bob\', \"lastName\"=\'Johnson\' WHERE \"id\"=1", "UPDATE \"myRecord\" SET \"lastName\"=\'Johnson\', \"firstName\"=\'Bob\' WHERE \"id\"=1"] .contains(lastSQLQuery)) } } } func testUpdateChangesWithNonRecordClass() throws { class MyRecord: Codable, PersistableRecord { var id: Int64? var firstName: String? var lastName: String? init(id: Int64?, firstName: String?, lastName: String?) { self.id = id self.firstName = firstName self.lastName = lastName } func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID } } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "myRecord") { t in t.autoIncrementedPrimaryKey("id") t.column("firstName", .text) t.column("lastName", .text) } var record = MyRecord(id: nil, firstName: "Arthur", lastName: "Smith") try record.insert(db) do { sqlQueries = [] let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = nil } XCTAssertEqual(record.firstName, nil) XCTAssertEqual(record.lastName, "Smith") XCTAssertTrue(modified) XCTAssertEqual(lastSQLQuery, "UPDATE \"myRecord\" SET \"firstName\"=NULL WHERE \"id\"=1") } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" } XCTAssertEqual(record.firstName, "Bob") XCTAssertEqual(record.lastName, "Johnson") XCTAssertTrue(modified) XCTAssertTrue([ "UPDATE \"myRecord\" SET \"firstName\"=\'Bob\', \"lastName\"=\'Johnson\' WHERE \"id\"=1", "UPDATE \"myRecord\" SET \"lastName\"=\'Johnson\', \"firstName\"=\'Bob\' WHERE \"id\"=1"] .contains(lastSQLQuery)) } } } func testUpdateChangesWithMutableStruct() throws { struct MyRecord: Codable, MutablePersistableRecord { var id: Int64? var firstName: String? var lastName: String? mutating func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID } } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "myRecord") { t in t.autoIncrementedPrimaryKey("id") t.column("firstName", .text) t.column("lastName", .text) } var record = MyRecord(id: nil, firstName: "Arthur", lastName: "Smith") try record.insert(db) do { sqlQueries = [] let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = nil } XCTAssertEqual(record.firstName, nil) XCTAssertEqual(record.lastName, "Smith") XCTAssertTrue(modified) XCTAssertEqual(lastSQLQuery, "UPDATE \"myRecord\" SET \"firstName\"=NULL WHERE \"id\"=1") } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" } XCTAssertEqual(record.firstName, "Bob") XCTAssertEqual(record.lastName, "Johnson") XCTAssertTrue(modified) XCTAssertTrue([ "UPDATE \"myRecord\" SET \"firstName\"=\'Bob\', \"lastName\"=\'Johnson\' WHERE \"id\"=1", "UPDATE \"myRecord\" SET \"lastName\"=\'Johnson\', \"firstName\"=\'Bob\' WHERE \"id\"=1"] .contains(lastSQLQuery)) } } } func testUpdateChangesWithImmutableStruct() throws { struct MyRecord: Codable, PersistableRecord { var id: Int64 var firstName: String? var lastName: String? } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "myRecord") { t in t.autoIncrementedPrimaryKey("id") t.column("firstName", .text) t.column("lastName", .text) } var record = MyRecord(id: 1, firstName: "Arthur", lastName: "Smith") try record.insert(db) do { sqlQueries = [] let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = nil } XCTAssertEqual(record.firstName, nil) XCTAssertEqual(record.lastName, "Smith") XCTAssertTrue(modified) XCTAssertEqual(lastSQLQuery, "UPDATE \"myRecord\" SET \"firstName\"=NULL WHERE \"id\"=1") } do { sqlQueries = [] let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" } XCTAssertEqual(record.firstName, "Bob") XCTAssertEqual(record.lastName, "Johnson") XCTAssertTrue(modified) XCTAssertTrue([ "UPDATE \"myRecord\" SET \"firstName\"=\'Bob\', \"lastName\"=\'Johnson\' WHERE \"id\"=1", "UPDATE \"myRecord\" SET \"lastName\"=\'Johnson\', \"firstName\"=\'Bob\' WHERE \"id\"=1"] .contains(lastSQLQuery)) } } } func testDatabaseChangesWithClass() throws { class Player: Encodable, EncodableRecord { var id: Int64 var name: String var score: Int init(id: Int64, name: String, score: Int) { self.id = id self.name = name self.score = score } } var player = Player(id: 1, name: "Arthur", score: 1000) do { let changes = try player.databaseChanges { _ in } XCTAssert(changes.isEmpty) } do { let changes = try player.databaseChanges { $0.name = "Barbara" } XCTAssertEqual(changes, ["name": "Arthur".databaseValue]) } do { let changes = try player.databaseChanges { $0.name = "Craig" $0.score = 200 } XCTAssertEqual(changes, ["name": "Barbara".databaseValue, "score": 1000.databaseValue]) } } func testDatabaseChangesWithStruct() throws { struct Player: Encodable, EncodableRecord { var id: Int64 var name: String var score: Int } var player = Player(id: 1, name: "Arthur", score: 1000) do { let changes = try player.databaseChanges { _ in } XCTAssert(changes.isEmpty) } do { let changes = try player.databaseChanges { $0.name = "Barbara" } XCTAssertEqual(changes, ["name": "Arthur".databaseValue]) } do { let changes = try player.databaseChanges { $0.name = "Craig" $0.score = 200 } XCTAssertEqual(changes, ["name": "Barbara".databaseValue, "score": 1000.databaseValue]) } } }