import XCTest import GRDB private struct MutablePersistableRecordPerson : MutablePersistableRecord { var id: Int64? var name: String? var age: Int? static let databaseTableName = "persons" func encode(to container: inout PersistenceContainer) { // mangle cases container["iD"] = id container["NAme"] = name container["aGe"] = age } func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws { let inserted = try insert() XCTAssertNotNil(inserted.rowID) XCTAssertEqual(inserted.rowIDColumn, "id") XCTAssertEqual(inserted.persistenceContainer["iD"]?.databaseValue, inserted.rowID.databaseValue) XCTAssertEqual(inserted.persistenceContainer["id"]?.databaseValue, inserted.rowID.databaseValue) } mutating func didInsert(_ inserted: InsertionSuccess) { XCTAssertEqual(inserted.rowIDColumn, "id") XCTAssertEqual(inserted.persistenceContainer["iD"]?.databaseValue, inserted.rowID.databaseValue) XCTAssertEqual(inserted.persistenceContainer["id"]?.databaseValue, inserted.rowID.databaseValue) id = inserted.rowID } } private struct MutablePersistableRecordCountry : MutablePersistableRecord { var rowID: Int64? var isoCode: String var name: String static let databaseTableName = "countries" func encode(to container: inout PersistenceContainer) { container["isoCode"] = isoCode container["name"] = name } mutating func didInsert(_ inserted: InsertionSuccess) { XCTAssertNil(inserted.rowIDColumn) rowID = inserted.rowID } } private class Callbacks { var willInsertCount = 0 var aroundInsertEnterCount = 0 var aroundInsertExitCount = 0 var didInsertCount = 0 var willUpdateCount = 0 var aroundUpdateEnterCount = 0 var aroundUpdateExitCount = 0 var didUpdateCount = 0 var willSaveCount = 0 var aroundSaveEnterCount = 0 var aroundSaveExitCount = 0 var didSaveCount = 0 var willDeleteCount = 0 var aroundDeleteEnterCount = 0 var aroundDeleteExitCount = 0 var didDeleteCount = 0 } private struct MutablePersistableRecordCustomizedCountry : MutablePersistableRecord { var rowID: Int64? var isoCode: String var name: String let callbacks = Callbacks() static let databaseTableName = "countries" func encode(to container: inout PersistenceContainer) { container["isoCode"] = isoCode container["name"] = name } mutating func willInsert(_ db: Database) throws { // Make sure database can be used try db.execute(sql: "SELECT 1") callbacks.willInsertCount += 1 } func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws { // Make sure database can be used try db.execute(sql: "SELECT 1") callbacks.aroundInsertEnterCount += 1 _ = try insert() callbacks.aroundInsertExitCount += 1 } mutating func didInsert(_ inserted: InsertionSuccess) { callbacks.didInsertCount += 1 rowID = inserted.rowID } func willUpdate(_ db: Database, columns: Set) throws { // Make sure database can be used try db.execute(sql: "SELECT 1") callbacks.willUpdateCount += 1 } func aroundUpdate(_ db: Database, columns: Set, update: () throws -> PersistenceSuccess) throws { // Make sure database can be used try db.execute(sql: "SELECT 1") callbacks.aroundUpdateEnterCount += 1 _ = try update() callbacks.aroundUpdateExitCount += 1 } func didUpdate(_ updated: PersistenceSuccess) { callbacks.didUpdateCount += 1 } func willSave(_ db: Database) throws { // Make sure database can be used try db.execute(sql: "SELECT 1") callbacks.willSaveCount += 1 } func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws { // Make sure database can be used try db.execute(sql: "SELECT 1") callbacks.aroundSaveEnterCount += 1 _ = try save() callbacks.aroundSaveExitCount += 1 } func didSave(_ saved: PersistenceSuccess) { callbacks.didSaveCount += 1 } func willDelete(_ db: Database) throws { // Make sure database can be used try db.execute(sql: "SELECT 1") callbacks.willDeleteCount += 1 } func aroundDelete(_ db: Database, delete: () throws -> Bool) throws { // Make sure database can be used try db.execute(sql: "SELECT 1") callbacks.aroundDeleteEnterCount += 1 _ = try delete() callbacks.aroundDeleteExitCount += 1 } func didDelete(deleted: Bool) { callbacks.didDeleteCount += 1 } } private struct PartialPlayer: Codable, MutablePersistableRecord, FetchableRecord { static let databaseTableName = "player" let callbacks = Callbacks() var id: Int64? var name: String enum CodingKeys: String, CodingKey { case id, name } mutating func willInsert(_ db: Database) throws { callbacks.willInsertCount += 1 } func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws { callbacks.aroundInsertEnterCount += 1 _ = try insert() callbacks.aroundInsertExitCount += 1 } mutating func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID callbacks.didInsertCount += 1 } func willUpdate(_ db: Database, columns: Set) throws { callbacks.willUpdateCount += 1 } func aroundUpdate(_ db: Database, columns: Set, update: () throws -> PersistenceSuccess) throws { callbacks.aroundUpdateEnterCount += 1 _ = try update() callbacks.aroundUpdateExitCount += 1 } func didUpdate(_ updated: PersistenceSuccess) { callbacks.didUpdateCount += 1 } func willSave(_ db: Database) throws { callbacks.willSaveCount += 1 } func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws { callbacks.aroundSaveEnterCount += 1 _ = try save() callbacks.aroundSaveExitCount += 1 } func didSave(_ saved: PersistenceSuccess) { callbacks.didSaveCount += 1 } func willDelete(_ db: Database) throws { callbacks.willDeleteCount += 1 } func aroundDelete(_ db: Database, delete: () throws -> Bool) throws { callbacks.aroundDeleteEnterCount += 1 _ = try delete() callbacks.aroundDeleteExitCount += 1 } func didDelete(deleted: Bool) { callbacks.didDeleteCount += 1 } } private struct FullPlayer: Codable, MutablePersistableRecord, FetchableRecord { static let databaseTableName = "player" var id: Int64? var name: String var score: Int enum CodingKeys: String, CodingKey { case id, name, score } let callbacks = Callbacks() mutating func willInsert(_ db: Database) throws { callbacks.willInsertCount += 1 } func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws { callbacks.aroundInsertEnterCount += 1 _ = try insert() callbacks.aroundInsertExitCount += 1 } mutating func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID callbacks.didInsertCount += 1 } func willUpdate(_ db: Database, columns: Set) throws { callbacks.willUpdateCount += 1 } func aroundUpdate(_ db: Database, columns: Set, update: () throws -> PersistenceSuccess) throws { callbacks.aroundUpdateEnterCount += 1 _ = try update() callbacks.aroundUpdateExitCount += 1 } func didUpdate(_ updated: PersistenceSuccess) { callbacks.didUpdateCount += 1 } func willSave(_ db: Database) throws { callbacks.willSaveCount += 1 } func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws { callbacks.aroundSaveEnterCount += 1 _ = try save() callbacks.aroundSaveExitCount += 1 } func didSave(_ saved: PersistenceSuccess) { callbacks.didSaveCount += 1 } func willDelete(_ db: Database) throws { callbacks.willDeleteCount += 1 } func aroundDelete(_ db: Database, delete: () throws -> Bool) throws { callbacks.aroundDeleteEnterCount += 1 _ = try delete() callbacks.aroundDeleteExitCount += 1 } func didDelete(deleted: Bool) { callbacks.didDeleteCount += 1 } } class MutablePersistableRecordTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() migrator.registerMigration("setUp") { db in try db.execute(sql: """ CREATE TABLE persons ( id INTEGER PRIMARY KEY, name NOT NULL, age INTEGER); CREATE TABLE countries ( isoCode TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL); CREATE TABLE player( id INTEGER PRIMARY KEY, name NOT NULL UNIQUE, -- UNIQUE for upsert tests score INTEGER NOT NULL DEFAULT 1000); """) } try migrator.migrate(dbWriter) } // MARK: - MutablePersistableRecordPerson func testInsertMutablePersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var person = MutablePersistableRecordPerson(id: nil, name: "Arthur", age: 24) try person.insert(db) let rows = try Row.fetchAll(db, sql: "SELECT * FROM persons") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["id"] as Int64, person.id!) XCTAssertEqual(rows[0]["name"] as String, "Arthur") } } func testInsertedMutablePersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person = try MutablePersistableRecordPerson(id: nil, name: "Arthur", age: 24).inserted(db) let rows = try Row.fetchAll(db, sql: "SELECT * FROM persons") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["id"] as Int64, person.id!) XCTAssertEqual(rows[0]["name"] as String, "Arthur") } } func testUpdateWithoutExplicitPrimaryKeyButWithExplicitRowIDSupport() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.execute(sql: "CREATE TABLE record(name)") struct Record: Codable, MutablePersistableRecord { var rowID: Int64? var name: String mutating func didInsert(_ inserted: InsertionSuccess) { rowID = inserted.rowID } } var record1 = Record(name: "Arthur") var record2 = Record(name: "Barbara") try record1.insert(db) try record2.insert(db) record1.name = "Craig" try record1.update(db) let names = try String.fetchAll(db, sql: "SELECT name FROM record ORDER BY rowid") XCTAssertEqual(names, ["Craig", "Barbara"]) } } func testUpdateWithoutExplicitPrimaryKeyAndWithoutExplicitRowIDSupport() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.execute(sql: "CREATE TABLE record(name)") struct Record: Codable, PersistableRecord { var name: String } let record = Record(name: "Arthur") try record.insert(db) do { try record.update(db) XCTFail("Expected RecordError") } catch RecordError.recordNotFound { } } } func testUpdateMutablePersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var person1 = MutablePersistableRecordPerson(id: nil, name: "Arthur", age: 24) try person1.insert(db) var person2 = MutablePersistableRecordPerson(id: nil, name: "Barbara", age: 24) try person2.insert(db) person1.name = "Craig" try person1.update(db) XCTAssertTrue([ "UPDATE \"persons\" SET \"age\"=24, \"name\"='Craig' WHERE \"id\"=1", "UPDATE \"persons\" SET \"name\"='Craig', \"age\"=24 WHERE \"id\"=1" ].contains(self.lastSQLQuery)) let rows = try Row.fetchAll(db, sql: "SELECT * FROM persons ORDER BY id") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["id"] as Int64, person1.id!) XCTAssertEqual(rows[0]["name"] as String, "Craig") XCTAssertEqual(rows[1]["id"] as Int64, person2.id!) XCTAssertEqual(rows[1]["name"] as String, "Barbara") } } func testPartialUpdateMutablePersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var person1 = MutablePersistableRecordPerson(id: nil, name: "Arthur", age: 24) try person1.insert(db) var person2 = MutablePersistableRecordPerson(id: nil, name: "Barbara", age: 36) try person2.insert(db) do { person1.name = "Craig" try person1.update(db, columns: [String]()) XCTAssertEqual(self.lastSQLQuery, "UPDATE \"persons\" SET \"id\"=1 WHERE \"id\"=1") let rows = try Row.fetchAll(db, sql: "SELECT * FROM persons ORDER BY id") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["id"] as Int64, person1.id!) XCTAssertEqual(rows[0]["name"] as String, "Arthur") XCTAssertEqual(rows[0]["age"] as Int, 24) XCTAssertEqual(rows[1]["id"] as Int64, person2.id!) XCTAssertEqual(rows[1]["name"] as String, "Barbara") XCTAssertEqual(rows[1]["age"] as Int, 36) } do { person1.name = "Craig" person1.age = 25 try person1.update(db, columns: [Column("name")]) XCTAssertEqual(self.lastSQLQuery, "UPDATE \"persons\" SET \"name\"='Craig' WHERE \"id\"=1") let rows = try Row.fetchAll(db, sql: "SELECT * FROM persons ORDER BY id") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["id"] as Int64, person1.id!) XCTAssertEqual(rows[0]["name"] as String, "Craig") XCTAssertEqual(rows[0]["age"] as Int, 24) XCTAssertEqual(rows[1]["id"] as Int64, person2.id!) XCTAssertEqual(rows[1]["name"] as String, "Barbara") XCTAssertEqual(rows[1]["age"] as Int, 36) } do { person1.name = "David" try person1.update(db, columns: ["AgE"]) // case insensitivity XCTAssertEqual(self.lastSQLQuery, "UPDATE \"persons\" SET \"age\"=25 WHERE \"id\"=1") let rows = try Row.fetchAll(db, sql: "SELECT * FROM persons ORDER BY id") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["id"] as Int64, person1.id!) XCTAssertEqual(rows[0]["name"] as String, "Craig") XCTAssertEqual(rows[0]["age"] as Int, 25) XCTAssertEqual(rows[1]["id"] as Int64, person2.id!) XCTAssertEqual(rows[1]["name"] as String, "Barbara") XCTAssertEqual(rows[1]["age"] as Int, 36) } } } func testSaveMutablePersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var person1 = MutablePersistableRecordPerson(id: nil, name: "Arthur", age: 24) try person1.save(db) var rows = try Row.fetchAll(db, sql: "SELECT * FROM persons") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["id"] as Int64, person1.id!) XCTAssertEqual(rows[0]["name"] as String, "Arthur") var person2 = MutablePersistableRecordPerson(id: nil, name: "Barbara", age: 24) try person2.save(db) person1.name = "Craig" try person1.save(db) rows = try Row.fetchAll(db, sql: "SELECT * FROM persons ORDER BY id") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["id"] as Int64, person1.id!) XCTAssertEqual(rows[0]["name"] as String, "Craig") XCTAssertEqual(rows[1]["id"] as Int64, person2.id!) XCTAssertEqual(rows[1]["name"] as String, "Barbara") try person1.delete(db) try person1.save(db) rows = try Row.fetchAll(db, sql: "SELECT * FROM persons ORDER BY id") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["id"] as Int64, person1.id!) XCTAssertEqual(rows[0]["name"] as String, "Craig") XCTAssertEqual(rows[1]["id"] as Int64, person2.id!) XCTAssertEqual(rows[1]["name"] as String, "Barbara") } } func testSavedMutablePersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var person1 = try MutablePersistableRecordPerson(id: nil, name: "Arthur", age: 24).saved(db) var rows = try Row.fetchAll(db, sql: "SELECT * FROM persons") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["id"] as Int64, person1.id!) XCTAssertEqual(rows[0]["name"] as String, "Arthur") let person2 = try MutablePersistableRecordPerson(id: nil, name: "Barbara", age: 24).saved(db) person1.name = "Craig" var savedPerson1 = try person1.saved(db) XCTAssertEqual(person1.id, savedPerson1.id) rows = try Row.fetchAll(db, sql: "SELECT * FROM persons ORDER BY id") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["id"] as Int64, person1.id!) XCTAssertEqual(rows[0]["name"] as String, "Craig") XCTAssertEqual(rows[1]["id"] as Int64, person2.id!) XCTAssertEqual(rows[1]["name"] as String, "Barbara") try person1.delete(db) savedPerson1 = try person1.saved(db) XCTAssertEqual(person1.id, savedPerson1.id) rows = try Row.fetchAll(db, sql: "SELECT * FROM persons ORDER BY id") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["id"] as Int64, savedPerson1.id!) XCTAssertEqual(rows[0]["name"] as String, "Craig") XCTAssertEqual(rows[1]["id"] as Int64, person2.id!) XCTAssertEqual(rows[1]["name"] as String, "Barbara") } } func testDeleteWithoutExplicitPrimaryKeyButWithExplicitRowIDSupport() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.execute(sql: "CREATE TABLE record(name)") struct Record: Codable, MutablePersistableRecord { var rowID: Int64? var name: String mutating func didInsert(_ inserted: InsertionSuccess) { rowID = inserted.rowID } } var record1 = Record(name: "Arthur") var record2 = Record(name: "Barbara") try record1.insert(db) try record2.insert(db) try record1.delete(db) let names = try String.fetchAll(db, sql: "SELECT name FROM record ORDER BY rowid") XCTAssertEqual(names, ["Barbara"]) } } func testDeleteWithoutExplicitPrimaryKeyAndWithoutExplicitRowIDSupport() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.execute(sql: "CREATE TABLE record(name)") struct Record: Codable, PersistableRecord { var name: String } let record = Record(name: "Arthur") try record.insert(db) try XCTAssertFalse(record.delete(db)) let names = try String.fetchAll(db, sql: "SELECT name FROM record ORDER BY rowid") XCTAssertEqual(names, ["Arthur"]) } } func testDeleteMutablePersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var person1 = MutablePersistableRecordPerson(id: nil, name: "Arthur", age: 24) try person1.insert(db) var person2 = MutablePersistableRecordPerson(id: nil, name: "Barbara", age: 24) try person2.insert(db) var deleted = try person1.delete(db) XCTAssertTrue(deleted) deleted = try person1.delete(db) XCTAssertFalse(deleted) let rows = try Row.fetchAll(db, sql: "SELECT * FROM persons ORDER BY id") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["id"] as Int64, person2.id!) XCTAssertEqual(rows[0]["name"] as String, "Barbara") } } func testExistsMutablePersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var person = MutablePersistableRecordPerson(id: nil, name: "Arthur", age: 24) try person.insert(db) XCTAssertTrue(try person.exists(db)) try person.delete(db) XCTAssertFalse(try person.exists(db)) } } func testMutablePersistableRecordPersonDatabaseDictionary() throws { let person = MutablePersistableRecordPerson(id: nil, name: "Arthur", age: 24) let dict = try person.databaseDictionary XCTAssertEqual(dict, ["iD": DatabaseValue.null, "NAme": "Arthur".databaseValue, "aGe": 24.databaseValue]) } // MARK: - MutablePersistableRecordCountry func testInsertMutablePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country = MutablePersistableRecordCountry(rowID: nil, isoCode: "FR", name: "France") try country.insert(db) let rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["rowID"] as Int64, country.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France") } } func testInsertedMutablePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let country = try MutablePersistableRecordCountry(rowID: nil, isoCode: "FR", name: "France").inserted(db) let rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["rowID"] as Int64, country.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France") } } func testUpdateMutablePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = MutablePersistableRecordCountry(rowID: nil, isoCode: "FR", name: "France") try country1.insert(db) var country2 = MutablePersistableRecordCountry(rowID: nil, isoCode: "US", name: "United States") try country2.insert(db) country1.name = "France Métropolitaine" try country1.update(db) XCTAssertEqual(self.lastSQLQuery, "UPDATE \"countries\" SET \"name\"='France Métropolitaine' WHERE \"isoCode\"='FR'") let rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[1]["name"] as String, "United States") } } func testSaveMutablePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = MutablePersistableRecordCountry(rowID: nil, isoCode: "FR", name: "France") try country1.save(db) var rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France") var country2 = MutablePersistableRecordCountry(rowID: nil, isoCode: "US", name: "United States") try country2.save(db) country1.name = "France Métropolitaine" try country1.save(db) rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[1]["name"] as String, "United States") try country1.delete(db) try country1.save(db) rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[0]["name"] as String, "United States") XCTAssertEqual(rows[1]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[1]["name"] as String, "France Métropolitaine") } } func testSavedMutablePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = try MutablePersistableRecordCountry(rowID: nil, isoCode: "FR", name: "France").saved(db) var rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France") let country2 = try MutablePersistableRecordCountry(rowID: nil, isoCode: "US", name: "United States").saved(db) country1.name = "France Métropolitaine" var savedCountry1 = try country1.saved(db) XCTAssertEqual(country1.rowID, savedCountry1.rowID) rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[1]["name"] as String, "United States") try country1.delete(db) savedCountry1 = try country1.saved(db) rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[0]["name"] as String, "United States") XCTAssertEqual(rows[1]["rowID"] as Int64, savedCountry1.rowID!) XCTAssertEqual(rows[1]["name"] as String, "France Métropolitaine") } } func testDeleteMutablePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = MutablePersistableRecordCountry(rowID: nil, isoCode: "FR", name: "France") try country1.insert(db) var country2 = MutablePersistableRecordCountry(rowID: nil, isoCode: "US", name: "United States") try country2.insert(db) var deleted = try country1.delete(db) XCTAssertTrue(deleted) deleted = try country1.delete(db) XCTAssertFalse(deleted) let rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[0]["name"] as String, "United States") } } func testExistsMutablePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country = MutablePersistableRecordCountry(rowID: nil, isoCode: "FR", name: "France") try country.insert(db) XCTAssertTrue(try country.exists(db)) try country.delete(db) XCTAssertFalse(try country.exists(db)) } } // MARK: - MutablePersistableRecordCustomizedCountry func testInsertMutablePersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country = MutablePersistableRecordCustomizedCountry( rowID: nil, isoCode: "FR", name: "France") try country.insert(db) XCTAssertEqual(country.callbacks.willInsertCount, 1) XCTAssertEqual(country.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(country.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(country.callbacks.didInsertCount, 1) XCTAssertEqual(country.callbacks.willUpdateCount, 0) XCTAssertEqual(country.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(country.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(country.callbacks.didUpdateCount, 0) XCTAssertEqual(country.callbacks.willSaveCount, 1) XCTAssertEqual(country.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(country.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(country.callbacks.didSaveCount, 1) XCTAssertEqual(country.callbacks.willDeleteCount, 0) XCTAssertEqual(country.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(country.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(country.callbacks.didDeleteCount, 0) let rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["rowID"] as Int64, country.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France") } } func testUpdateMutablePersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = MutablePersistableRecordCustomizedCountry( rowID: nil, isoCode: "FR", name: "France") try country1.insert(db) var country2 = MutablePersistableRecordCustomizedCountry( rowID: nil, isoCode: "US", name: "United States") try country2.insert(db) country1.name = "France Métropolitaine" try country1.update(db) XCTAssertEqual(self.lastSQLQuery, "UPDATE \"countries\" SET \"name\"='France Métropolitaine' WHERE \"isoCode\"='FR'") XCTAssertEqual(country1.callbacks.willInsertCount, 1) XCTAssertEqual(country1.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(country1.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(country1.callbacks.didInsertCount, 1) XCTAssertEqual(country1.callbacks.willUpdateCount, 1) XCTAssertEqual(country1.callbacks.aroundUpdateEnterCount, 1) XCTAssertEqual(country1.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(country1.callbacks.didUpdateCount, 1) XCTAssertEqual(country1.callbacks.willSaveCount, 2) XCTAssertEqual(country1.callbacks.aroundSaveEnterCount, 2) XCTAssertEqual(country1.callbacks.aroundSaveExitCount, 2) XCTAssertEqual(country1.callbacks.didSaveCount, 2) XCTAssertEqual(country1.callbacks.willDeleteCount, 0) XCTAssertEqual(country1.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(country1.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(country1.callbacks.didDeleteCount, 0) let rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[1]["name"] as String, "United States") } } func testRecordErrorMutablePersistableRecordCustomizedCountry() throws { let country = MutablePersistableRecordCustomizedCountry( rowID: nil, isoCode: "FR", name: "France") let dbQueue = try makeDatabaseQueue() do { try dbQueue.inDatabase { db in try country.update(db) } XCTFail("Expected RecordError") } catch RecordError.recordNotFound(databaseTableName: "countries", key: ["isoCode": "FR".databaseValue]) { } XCTAssertEqual(country.callbacks.willInsertCount, 0) XCTAssertEqual(country.callbacks.aroundInsertEnterCount, 0) XCTAssertEqual(country.callbacks.aroundInsertExitCount, 0) XCTAssertEqual(country.callbacks.didInsertCount, 0) XCTAssertEqual(country.callbacks.willUpdateCount, 1) XCTAssertEqual(country.callbacks.aroundUpdateEnterCount, 1) XCTAssertEqual(country.callbacks.aroundUpdateExitCount, 0) // last update has failed XCTAssertEqual(country.callbacks.didUpdateCount, 0) // last update has failed XCTAssertEqual(country.callbacks.willSaveCount, 1) XCTAssertEqual(country.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(country.callbacks.aroundSaveExitCount, 0) // last update has failed XCTAssertEqual(country.callbacks.didSaveCount, 0) // last update has failed XCTAssertEqual(country.callbacks.willDeleteCount, 0) XCTAssertEqual(country.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(country.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(country.callbacks.didDeleteCount, 0) } func testSaveMutablePersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = MutablePersistableRecordCustomizedCountry( rowID: nil, isoCode: "FR", name: "France") try country1.save(db) XCTAssertEqual(country1.callbacks.willInsertCount, 1) XCTAssertEqual(country1.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(country1.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(country1.callbacks.didInsertCount, 1) XCTAssertEqual(country1.callbacks.willUpdateCount, 1) XCTAssertEqual(country1.callbacks.aroundUpdateEnterCount, 1) XCTAssertEqual(country1.callbacks.aroundUpdateExitCount, 0) // last update has failed XCTAssertEqual(country1.callbacks.didUpdateCount, 0) // last update has failed XCTAssertEqual(country1.callbacks.willSaveCount, 1) XCTAssertEqual(country1.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(country1.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(country1.callbacks.didSaveCount, 1) XCTAssertEqual(country1.callbacks.willDeleteCount, 0) XCTAssertEqual(country1.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(country1.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(country1.callbacks.didDeleteCount, 0) var rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France") var country2 = MutablePersistableRecordCustomizedCountry( rowID: nil, isoCode: "US", name: "United States") try country2.save(db) country1.name = "France Métropolitaine" try country1.save(db) XCTAssertEqual(country1.callbacks.willInsertCount, 1) XCTAssertEqual(country1.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(country1.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(country1.callbacks.didInsertCount, 1) XCTAssertEqual(country1.callbacks.willUpdateCount, 2) XCTAssertEqual(country1.callbacks.aroundUpdateEnterCount, 2) XCTAssertEqual(country1.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(country1.callbacks.didUpdateCount, 1) XCTAssertEqual(country1.callbacks.willSaveCount, 2) XCTAssertEqual(country1.callbacks.aroundSaveEnterCount, 2) XCTAssertEqual(country1.callbacks.aroundSaveExitCount, 2) XCTAssertEqual(country1.callbacks.didSaveCount, 2) XCTAssertEqual(country1.callbacks.willDeleteCount, 0) XCTAssertEqual(country1.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(country1.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(country1.callbacks.didDeleteCount, 0) rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[1]["name"] as String, "United States") _ = try country1.delete(db) try country1.save(db) XCTAssertEqual(country1.callbacks.willInsertCount, 2) XCTAssertEqual(country1.callbacks.aroundInsertEnterCount, 2) XCTAssertEqual(country1.callbacks.aroundInsertExitCount, 2) XCTAssertEqual(country1.callbacks.didInsertCount, 2) XCTAssertEqual(country1.callbacks.willUpdateCount, 3) XCTAssertEqual(country1.callbacks.aroundUpdateEnterCount, 3) XCTAssertEqual(country1.callbacks.aroundUpdateExitCount, 1) // last update has failed XCTAssertEqual(country1.callbacks.didUpdateCount, 1) // last update has failed XCTAssertEqual(country1.callbacks.willSaveCount, 3) XCTAssertEqual(country1.callbacks.aroundSaveEnterCount, 3) XCTAssertEqual(country1.callbacks.aroundSaveExitCount, 3) XCTAssertEqual(country1.callbacks.didSaveCount, 3) XCTAssertEqual(country1.callbacks.willDeleteCount, 1) XCTAssertEqual(country1.callbacks.aroundDeleteEnterCount, 1) XCTAssertEqual(country1.callbacks.aroundDeleteExitCount, 1) XCTAssertEqual(country1.callbacks.didDeleteCount, 1) rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[0]["name"] as String, "United States") XCTAssertEqual(rows[1]["rowID"] as Int64, country1.rowID!) XCTAssertEqual(rows[1]["name"] as String, "France Métropolitaine") } } func testDeleteMutablePersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = MutablePersistableRecordCustomizedCountry( rowID: nil, isoCode: "FR", name: "France") try country1.insert(db) var country2 = MutablePersistableRecordCustomizedCountry( rowID: nil, isoCode: "US", name: "United States") try country2.insert(db) var deleted = try country1.delete(db) XCTAssertTrue(deleted) deleted = try country1.delete(db) XCTAssertFalse(deleted) XCTAssertEqual(country1.callbacks.willInsertCount, 1) XCTAssertEqual(country1.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(country1.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(country1.callbacks.didInsertCount, 1) XCTAssertEqual(country1.callbacks.willUpdateCount, 0) XCTAssertEqual(country1.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(country1.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(country1.callbacks.didUpdateCount, 0) XCTAssertEqual(country1.callbacks.willSaveCount, 1) XCTAssertEqual(country1.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(country1.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(country1.callbacks.didSaveCount, 1) XCTAssertEqual(country1.callbacks.willDeleteCount, 2) XCTAssertEqual(country1.callbacks.aroundDeleteEnterCount, 2) XCTAssertEqual(country1.callbacks.aroundDeleteExitCount, 2) XCTAssertEqual(country1.callbacks.didDeleteCount, 2) let rows = try Row.fetchAll(db, sql: "SELECT rowID, * FROM countries ORDER BY rowID") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["rowID"] as Int64, country2.rowID!) XCTAssertEqual(rows[0]["name"] as String, "United States") } } func testExistsMutablePersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country = MutablePersistableRecordCustomizedCountry( rowID: nil, isoCode: "FR", name: "France") try country.insert(db) XCTAssertTrue(try country.exists(db)) XCTAssertEqual(country.callbacks.willInsertCount, 1) XCTAssertEqual(country.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(country.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(country.callbacks.didInsertCount, 1) XCTAssertEqual(country.callbacks.willUpdateCount, 0) XCTAssertEqual(country.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(country.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(country.callbacks.didUpdateCount, 0) XCTAssertEqual(country.callbacks.willSaveCount, 1) XCTAssertEqual(country.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(country.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(country.callbacks.didSaveCount, 1) XCTAssertEqual(country.callbacks.willDeleteCount, 0) XCTAssertEqual(country.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(country.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(country.callbacks.didDeleteCount, 0) _ = try country.delete(db) XCTAssertFalse(try country.exists(db)) XCTAssertEqual(country.callbacks.willInsertCount, 1) XCTAssertEqual(country.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(country.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(country.callbacks.didInsertCount, 1) XCTAssertEqual(country.callbacks.willUpdateCount, 0) XCTAssertEqual(country.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(country.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(country.callbacks.didUpdateCount, 0) XCTAssertEqual(country.callbacks.willSaveCount, 1) XCTAssertEqual(country.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(country.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(country.callbacks.didSaveCount, 1) XCTAssertEqual(country.callbacks.willDeleteCount, 1) XCTAssertEqual(country.callbacks.aroundDeleteEnterCount, 1) XCTAssertEqual(country.callbacks.aroundDeleteExitCount, 1) XCTAssertEqual(country.callbacks.didDeleteCount, 1) } } // MARK: - Misc func testPartiallyEncodedRecord() throws { struct PartialRecord : MutablePersistableRecord { var id: Int64? var a: String static let databaseTableName = "records" func encode(to container: inout PersistenceContainer) { container["id"] = id container["a"] = a } mutating func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID } } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "records") { t in t.primaryKey("id", .integer) t.column("a", .text) t.column("b", .text) t.column("c", .integer).notNull().defaults(to: 123) } // Insertion only inserts defined columns var record = PartialRecord(id: nil, a: "foo") try record.insert(db) XCTAssertTrue( ["INSERT INTO \"records\" (\"id\", \"a\") VALUES (NULL,'foo')", "INSERT INTO \"records\" (\"a\", \"id\") VALUES ('foo',NULL)"] .contains(lastSQLQuery)) XCTAssertEqual(try Row.fetchOne(db, sql: "SELECT * FROM records")!, ["id": 1, "a": "foo", "b": nil, "c": 123]) // Update only updates defined columns record.a = "bar" try record.update(db) XCTAssertEqual(lastSQLQuery, "UPDATE \"records\" SET \"a\"='bar' WHERE \"id\"=1") XCTAssertEqual(try Row.fetchOne(db, sql: "SELECT * FROM records")!, ["id": 1, "a": "bar", "b": nil, "c": 123]) // Update always update something record.a = "baz" try record.update(db, columns: ["b"]) XCTAssertEqual(lastSQLQuery, "UPDATE \"records\" SET \"id\"=1 WHERE \"id\"=1") XCTAssertEqual(try Row.fetchOne(db, sql: "SELECT * FROM records")!, ["id": 1, "a": "bar", "b": nil, "c": 123]) // Deletion try record.delete(db) XCTAssertEqual(lastSQLQuery, "DELETE FROM \"records\" WHERE \"id\"=1") XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM records")!, 0) // Expect database errors when missing columns must have a value try db.drop(table: "records") try db.create(table: "records") { t in t.primaryKey("id", .integer) t.column("a", .text) t.column("b", .text).notNull() } do { try record.insert(db) } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT) // actual error message depends on the SQLite version XCTAssertTrue( ["NOT NULL constraint failed: records.b", "records.b may not be NULL"].contains(error.message!)) } } } func testRecordErrorRecordNotFoundDescription() { do { let error = RecordError.recordNotFound( databaseTableName: "place", key: ["id": .null]) XCTAssertEqual( error.description, "Key not found in table place: [id:NULL]") } do { let error = RecordError.recordNotFound( databaseTableName: "user", key: ["uuid": "E621E1F8-C36C-495A-93FC-0C247A3E6E5F".databaseValue]) XCTAssertEqual( error.description, "Key not found in table user: [uuid:\"E621E1F8-C36C-495A-93FC-0C247A3E6E5F\"]") } } func testGeneratedColumnsInsertIsAnError() throws { #if !GRDBCUSTOMSQLITE throw XCTSkip("Generated columns are not available") #else struct T: MutablePersistableRecord { func encode(to container: inout PersistenceContainer) { container["a"] = 1 container["b"] = 1 } } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.execute(sql: "CREATE TABLE t (a, b ALWAYS GENERATED AS (a))") do { var record = T() try record.insert(db) XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.message!, "cannot INSERT into generated column \"b\"") XCTAssertEqual(error.sql!, "INSERT INTO \"t\" (\"a\", \"b\") VALUES (?,?)") } } #endif } func testGeneratedColumnsUpdateIsAnError() throws { #if !GRDBCUSTOMSQLITE throw XCTSkip("Generated columns are not available") #else struct T: MutablePersistableRecord { func encode(to container: inout PersistenceContainer) { container["id"] = 1 container["a"] = 1 } } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.execute(sql: "CREATE TABLE t (id INTEGER PRIMARY KEY, a ALWAYS GENERATED AS (id))") do { try T().update(db) XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.message!, "cannot UPDATE generated column \"a\"") XCTAssertEqual(error.sql!, "UPDATE \"t\" SET \"a\"=? WHERE \"id\"=?") } } #endif } } // MARK: - Insert and Fetch extension MutablePersistableRecordTests { func test_insertAndFetch() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let player = FullPlayer(id: nil, name: "Arthur", score: 1000) let insertedPlayer = try XCTUnwrap(player.insertAndFetch(db)) XCTAssertEqual(insertedPlayer.id, 1) XCTAssertEqual(insertedPlayer.name, "Arthur") XCTAssertEqual(insertedPlayer.score, 1000) } } func test_insertAndFetch_as() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try XCTUnwrap(partialPlayer.insertAndFetch(db, as: FullPlayer.self)) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING * """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(partialPlayer.id, 1) XCTAssertEqual(fullPlayer.id, 1) XCTAssertEqual(fullPlayer.name, "Arthur") XCTAssertEqual(fullPlayer.score, 1000) XCTAssertEqual(partialPlayer.callbacks.willInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.willUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.willSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.willDeleteCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didDeleteCount, 0) } } } func test_insertAndFetch_selection_fetch() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement)! } XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING "score" """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(partialPlayer.id, 1) XCTAssertEqual(score, 1000) XCTAssertEqual(partialPlayer.callbacks.willInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.willUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.willSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.willDeleteCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didDeleteCount, 0) } do { // Test onConflict: .ignore sqlQueries.removeAll() var player = FullPlayer(id: 1, name: "Barbara", score: 100) try XCTAssertTrue(player.exists(db)) let score = try player.insertAndFetch(db, onConflict: .ignore, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) } XCTAssert(sqlQueries.contains(""" INSERT OR IGNORE INTO "player" ("id", "name", "score") VALUES (1,'Barbara',100) RETURNING "score" """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(player.id, 1) XCTAssertNil(score) XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(player.callbacks.didUpdateCount, 0) XCTAssertEqual(player.callbacks.willSaveCount, 1) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(player.callbacks.didSaveCount, 1) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } } } // MARK: - Save and Fetch extension MutablePersistableRecordTests { func test_saveAndFetch() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let player = FullPlayer(id: nil, name: "Arthur", score: 1000) let savedPlayer = try XCTUnwrap(player.saveAndFetch(db)) XCTAssertEqual(savedPlayer.id, 1) XCTAssertEqual(savedPlayer.name, "Arthur") XCTAssertEqual(savedPlayer.score, 1000) } } func test_saveAndFetch_as() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) XCTAssert(sqlQueries.allSatisfy { !$0.contains("UPDATE") }) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING * """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(partialPlayer.id, 1) XCTAssertEqual(fullPlayer.id, 1) XCTAssertEqual(fullPlayer.name, "Arthur") XCTAssertEqual(fullPlayer.score, 1000) XCTAssertEqual(partialPlayer.callbacks.willInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.willUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.willSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.willDeleteCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didDeleteCount, 0) } do { var partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) sqlQueries.removeAll() let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING * """), sqlQueries.joined(separator: "\n")) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (1,'Arthur') RETURNING * """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(partialPlayer.id, 1) XCTAssertEqual(fullPlayer.id, 1) XCTAssertEqual(fullPlayer.name, "Arthur") XCTAssertEqual(fullPlayer.score, 1000) XCTAssertEqual(partialPlayer.callbacks.willInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.willUpdateCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.willSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.willDeleteCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didDeleteCount, 1) } do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(id: 1, name: "Arthur") let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) XCTAssert(sqlQueries.allSatisfy { !$0.contains("INSERT") }) XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING * """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(partialPlayer.id, 1) XCTAssertEqual(fullPlayer.id, 1) XCTAssertEqual(fullPlayer.name, "Arthur") XCTAssertEqual(fullPlayer.score, 1000) XCTAssertEqual(partialPlayer.callbacks.willInsertCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundInsertEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundInsertExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didInsertCount, 0) XCTAssertEqual(partialPlayer.callbacks.willUpdateCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didUpdateCount, 1) XCTAssertEqual(partialPlayer.callbacks.willSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.willDeleteCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didDeleteCount, 0) } } } func test_saveAndFetch_selection_fetch() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) } XCTAssert(sqlQueries.allSatisfy { !$0.contains("UPDATE") }) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING "score" """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(partialPlayer.id, 1) XCTAssertEqual(score, 1000) XCTAssertEqual(partialPlayer.callbacks.willInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.willUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.willSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.willDeleteCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didDeleteCount, 0) } do { var partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) sqlQueries.removeAll() let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) } XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING "score" """), sqlQueries.joined(separator: "\n")) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (1,'Arthur') RETURNING "score" """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(partialPlayer.id, 1) XCTAssertEqual(score, 1000) XCTAssertEqual(partialPlayer.callbacks.willInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.willUpdateCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.willSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.willDeleteCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didDeleteCount, 1) } do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(id: 1, name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) } XCTAssert(sqlQueries.allSatisfy { !$0.contains("INSERT") }) XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING "score" """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(partialPlayer.id, 1) XCTAssertEqual(score, 1000) XCTAssertEqual(partialPlayer.callbacks.willInsertCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundInsertEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundInsertExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didInsertCount, 0) XCTAssertEqual(partialPlayer.callbacks.willUpdateCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didUpdateCount, 1) XCTAssertEqual(partialPlayer.callbacks.willSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.willDeleteCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didDeleteCount, 0) } } } } // MARK: - Update and Fetch extension MutablePersistableRecordTests { func test_updateAndFetch() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var player = FullPlayer(id: 1, name: "Arthur", score: 1000) do { _ = try player.updateAndFetch(db) XCTFail("Expected RecordError") } catch RecordError.recordNotFound(databaseTableName: "player", key: ["id": 1.databaseValue]) { } try player.insert(db) player.name = "Barbara" do { let updatedPlayer = try XCTUnwrap(player.updateAndFetch(db)) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Barbara") XCTAssertEqual(updatedPlayer.score, 1000) } XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(player.callbacks.didUpdateCount, 1) XCTAssertEqual(player.callbacks.willSaveCount, 3) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 3) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 2) XCTAssertEqual(player.callbacks.didSaveCount, 2) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } func test_updateAndFetch_as() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var player = FullPlayer(id: 1, name: "Arthur", score: 1000) do { _ = try player.updateAndFetch(db, as: PartialPlayer.self) XCTFail("Expected RecordError") } catch RecordError.recordNotFound(databaseTableName: "player", key: ["id": 1.databaseValue]) { } try player.insert(db) player.name = "Barbara" do { let updatedPlayer = try XCTUnwrap(player.updateAndFetch(db, as: PartialPlayer.self)) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Barbara") } XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(player.callbacks.didUpdateCount, 1) XCTAssertEqual(player.callbacks.willSaveCount, 3) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 3) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 2) XCTAssertEqual(player.callbacks.didSaveCount, 2) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } func test_updateAndFetch_selection_fetch() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var player = FullPlayer(id: 1, name: "Arthur", score: 1000) do { _ = try player.updateAndFetch(db, selection: [AllColumns()]) { statement in try Row.fetchOne(statement) } XCTFail("Expected RecordError") } catch RecordError.recordNotFound(databaseTableName: "player", key: ["id": 1.databaseValue]) { } try player.insert(db) player.name = "Barbara" player.score = 0 do { let row = try player.updateAndFetch(db, selection: [AllColumns()]) { statement in try Row.fetchOne(statement) } XCTAssertEqual(row, ["id": 1, "name": "Barbara", "score": 0]) } XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(player.callbacks.didUpdateCount, 1) XCTAssertEqual(player.callbacks.willSaveCount, 3) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 3) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 2) XCTAssertEqual(player.callbacks.didSaveCount, 2) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } func test_updateAndFetch_columns_selection_fetch() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var player = FullPlayer(id: 1, name: "Arthur", score: 1000) do { _ = try player.updateAndFetch(db, columns: [Column("score")], selection: [AllColumns()]) { statement in try Row.fetchOne(statement) } XCTFail("Expected RecordError") } catch RecordError.recordNotFound(databaseTableName: "player", key: ["id": 1.databaseValue]) { } try player.insert(db) player.name = "Barbara" player.score = 0 do { let row = try player.updateAndFetch(db, columns: [Column("score")], selection: [AllColumns()]) { statement in try Row.fetchOne(statement) } XCTAssertEqual(row, ["id": 1, "name": "Arthur", "score": 0]) } XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(player.callbacks.didUpdateCount, 1) XCTAssertEqual(player.callbacks.willSaveCount, 3) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 3) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 2) XCTAssertEqual(player.callbacks.didSaveCount, 2) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } func test_updateChangesAndFetch_modify() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var player = FullPlayer(id: 1, name: "Arthur", score: 1000) do { _ = try player.updateChangesAndFetch(db) { $0.name = "Barbara" } XCTFail("Expected RecordError") } catch RecordError.recordNotFound(databaseTableName: "player", key: ["id": 1.databaseValue]) { } try player.insert(db) do { let updatedPlayer = try player.updateChangesAndFetch(db) { $0.name = "Barbara" } XCTAssertNil(updatedPlayer) } do { let updatedPlayer = try XCTUnwrap(player.updateChangesAndFetch(db) { $0.name = "Craig" }) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Craig") XCTAssertEqual(updatedPlayer.score, 1000) } XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(player.callbacks.didUpdateCount, 1) XCTAssertEqual(player.callbacks.willSaveCount, 3) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 3) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 2) XCTAssertEqual(player.callbacks.didSaveCount, 2) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } func test_updateChangesAndFetch_as_modify() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var player = FullPlayer(id: 1, name: "Arthur", score: 1000) do { _ = try player.updateChangesAndFetch(db, as: PartialPlayer.self) { $0.name = "Barbara" } XCTFail("Expected RecordError") } catch RecordError.recordNotFound(databaseTableName: "player", key: ["id": 1.databaseValue]) { } try player.insert(db) do { let updatedPlayer = try player.updateChangesAndFetch(db, as: PartialPlayer.self) { $0.name = "Barbara" } XCTAssertNil(updatedPlayer) } do { let updatedPlayer = try XCTUnwrap(player.updateChangesAndFetch(db, as: PartialPlayer.self) { $0.name = "Craig" }) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Craig") } XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(player.callbacks.didUpdateCount, 1) XCTAssertEqual(player.callbacks.willSaveCount, 3) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 3) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 2) XCTAssertEqual(player.callbacks.didSaveCount, 2) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } func test_updateChangesAndFetch_selection_fetch_modify() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("RETURNING clause is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("RETURNING clause is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var player = FullPlayer(id: 1, name: "Arthur", score: 1000) do { _ = try player.updateChangesAndFetch( db, selection: [AllColumns()], fetch: { statement in try Row.fetchOne(statement) }, modify: { $0.name = "Barbara" }) XCTFail("Expected RecordError") } catch RecordError.recordNotFound(databaseTableName: "player", key: ["id": 1.databaseValue]) { } try player.insert(db) do { let updatedRow = try player.updateChangesAndFetch( db, selection: [AllColumns()], fetch: { statement in try Row.fetchOne(statement) }, modify: { $0.name = "Barbara" }) XCTAssertNil(updatedRow) } do { let updatedRow = try player.updateChangesAndFetch( db, selection: [AllColumns()], fetch: { statement in try Row.fetchOne(statement) }, modify: { $0.name = "Craig" }) XCTAssertEqual(updatedRow, ["id": 1, "name": "Craig", "score": 1000]) } XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 2) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 1) XCTAssertEqual(player.callbacks.didUpdateCount, 1) XCTAssertEqual(player.callbacks.willSaveCount, 3) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 3) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 2) XCTAssertEqual(player.callbacks.didSaveCount, 2) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } } // MARK: - Upsert extension MutablePersistableRecordTests { func test_upsert() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("UPSERT is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("UPSERT is not available") } #endif try makeDatabaseQueue().write { db in do { var player = FullPlayer(name: "Arthur", score: 1000) try player.upsert(db) // Test SQL XCTAssertEqual(lastSQLQuery, """ INSERT INTO "player" ("id", "name", "score") \ VALUES (NULL,'Arthur',1000) \ ON CONFLICT DO UPDATE SET "name" = "excluded"."name", "score" = "excluded"."score" \ RETURNING "rowid" """) // Test database state let rows = try Row.fetchAll(db, FullPlayer.orderByPrimaryKey()) XCTAssertEqual(rows, [ ["id": 1, "name": "Arthur", "score":1000], ]) // Test didSave callback XCTAssertEqual(player.id, 1) // Test other callbacks XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(player.callbacks.didUpdateCount, 0) XCTAssertEqual(player.callbacks.willSaveCount, 1) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(player.callbacks.didSaveCount, 1) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } // Test conflict on name do { // Set the last inserted row id to some arbitrary value _ = try FullPlayer(id: 42, name: "Barbara", score: 0).inserted(db) XCTAssertNotEqual(db.lastInsertedRowID, 1) var player = FullPlayer(name: "Arthur", score: 100) try player.upsert(db) // Test database state let rows = try Row.fetchAll(db, FullPlayer.orderByPrimaryKey()) XCTAssertEqual(rows, [ ["id": 1, "name": "Arthur", "score":100], ["id": 42, "name": "Barbara", "score":0], ]) // Test didSave callback XCTAssertEqual(player.id, 1) } // Test conflict on id do { var player = FullPlayer(id: 1, name: "Craig", score: 500) try player.upsert(db) // Test database state let rows = try Row.fetchAll(db, FullPlayer.orderByPrimaryKey()) XCTAssertEqual(rows, [ ["id": 1, "name": "Craig", "score":500], ["id": 42, "name": "Barbara", "score":0], ]) // Test didSave callback XCTAssertEqual(player.id, 1) } // Test conflict on both id and name (same row) do { var player = FullPlayer(id: 1, name: "Craig", score: 200) try player.upsert(db) // Test database state let rows = try Row.fetchAll(db, FullPlayer.orderByPrimaryKey()) XCTAssertEqual(rows, [ ["id": 1, "name": "Craig", "score":200], ["id": 42, "name": "Barbara", "score":0], ]) // Test didSave callback XCTAssertEqual(player.id, 1) } // Test conflict on both id and name (different rows) do { var player = FullPlayer(id: 1, name: "Barbara", score: 300) do { try player.upsert(db) XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT) XCTAssertEqual(error.message, "UNIQUE constraint failed: player.name") XCTAssertEqual(error.sql!, """ INSERT INTO "player" ("id", "name", "score") \ VALUES (?,?,?) \ ON CONFLICT DO UPDATE SET "name" = "excluded"."name", "score" = "excluded"."score" \ RETURNING "rowid" """) } // Test callbacks XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 0) XCTAssertEqual(player.callbacks.didInsertCount, 0) XCTAssertEqual(player.callbacks.willUpdateCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(player.callbacks.didUpdateCount, 0) XCTAssertEqual(player.callbacks.willSaveCount, 1) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 0) XCTAssertEqual(player.callbacks.didSaveCount, 0) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } } func test_upsertAndFetch_do_update_set_where() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("UPSERT is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("UPSERT is not available") } #endif try makeDatabaseQueue().write { db in // Test exemples of https://www.sqlite.org/lang_UPSERT.html do { try db.execute(sql: """ CREATE TABLE vocabulary( word TEXT PRIMARY KEY, kind TEXT, isTainted BOOLEAN DEFAULT 0, count INT DEFAULT 1); INSERT INTO vocabulary(word, isTainted) VALUES('jovial', 1); """) struct Vocabulary: Decodable, MutablePersistableRecord, FetchableRecord { var word: String var kind: String var isTainted: Bool var count: Int? var rowID: Int64? func encode(to container: inout PersistenceContainer) { // Don't encode count and rowID container["word"] = word container["kind"] = kind container["isTainted"] = isTainted } mutating func didInsert(_ inserted: InsertionSuccess) { rowID = inserted.rowID } } // One column with specific assignment (count) // One column with no assignment (isTainted) // One column with default overwrite assignment (kind) do { var vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false) let upserted = try vocabulary.upsertAndFetch( db, onConflict: ["word"], doUpdate: { _ in [Column("count") += 1, // increment count Column("isTainted").noOverwrite] // don't overwrite isTainted }) // Test didSave XCTAssertEqual(vocabulary.rowID, 1) // Test SQL XCTAssertEqual(lastSQLQuery, """ INSERT INTO "vocabulary" ("word", "kind", "isTainted") \ VALUES ('jovial','adjective',0) \ ON CONFLICT("word") \ DO UPDATE SET "count" = "count" + 1, "kind" = "excluded"."kind" \ RETURNING *, "rowid" """) // Test database state let rows = try Row.fetchAll(db, sql: "SELECT * FROM vocabulary") XCTAssertEqual(rows, [ ["word": "jovial", "kind": "adjective", "isTainted": 1, "count": 2], ]) // Test upserted record XCTAssertEqual(upserted.word, "jovial") XCTAssertEqual(upserted.kind, "adjective") XCTAssertEqual(upserted.isTainted, true) // Not overwritten XCTAssertEqual(upserted.count, 2) // incremented } // All columns with no assignment: make sure we return something do { var vocabulary = Vocabulary(word: "jovial", kind: "ignored", isTainted: false) let upserted = try vocabulary.upsertAndFetch( db, onConflict: ["word"], doUpdate: { _ in [Column("count").noOverwrite, Column("isTainted").noOverwrite, Column("kind").noOverwrite] }) // Test didSave XCTAssertEqual(vocabulary.rowID, 1) // Test SQL (the DO UPDATE clause is not empty, so that the // RETURNING clause could return something). XCTAssertEqual(lastSQLQuery, """ INSERT INTO "vocabulary" ("word", "kind", "isTainted") \ VALUES ('jovial','ignored',0) \ ON CONFLICT("word") \ DO UPDATE SET "word" = "word" \ RETURNING *, "rowid" """) // Test database state let rows = try Row.fetchAll(db, sql: "SELECT * FROM vocabulary") XCTAssertEqual(rows, [ ["word": "jovial", "kind": "adjective", "isTainted": 1, "count": 2], ]) // Test upserted record XCTAssertEqual(upserted.word, "jovial") XCTAssertEqual(upserted.kind, "adjective") XCTAssertEqual(upserted.isTainted, true) XCTAssertEqual(upserted.count, 2) } } do { try db.execute(sql: """ CREATE TABLE phonebook(name TEXT PRIMARY KEY, phonenumber TEXT); INSERT INTO phonebook(name,phonenumber) VALUES('Alice','ignored'); """) struct Phonebook: Codable, MutablePersistableRecord, FetchableRecord { var name: String var phonenumber: String } var phonebook = Phonebook(name: "Alice", phonenumber: "704-555-1212") let upserted = try phonebook.upsertAndFetch( db, onConflict: ["name"], doUpdate: { excluded in [Column("phonenumber").set(to: excluded["phonenumber"])] }) // Test SQL XCTAssertEqual(lastSQLQuery, """ INSERT INTO "phonebook" ("name", "phonenumber") \ VALUES ('Alice','704-555-1212') \ ON CONFLICT("name") DO UPDATE SET "phonenumber" = "excluded"."phonenumber" \ RETURNING *, "rowid" """) // Test database state let rows = try Row.fetchAll(db, sql: "SELECT * FROM phonebook") XCTAssertEqual(rows, [ ["name": "Alice", "phonenumber": "704-555-1212"], ]) // Test upserted record XCTAssertEqual(upserted.name, "Alice") XCTAssertEqual(upserted.phonenumber, "704-555-1212") } } } func test_upsertAndFetch() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("UPSERT is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("UPSERT is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { sqlQueries.removeAll() var player = FullPlayer(id: 1, name: "Arthur", score: 1000) let upsertedPlayer = try player.upsertAndFetch(db) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name", "score") \ VALUES (1,'Arthur',1000) \ ON CONFLICT DO UPDATE SET "name" = "excluded"."name", "score" = "excluded"."score" \ RETURNING *, "rowid" """), sqlQueries.joined(separator: "\n")) // Test database state let rows = try Row.fetchAll(db, FullPlayer.orderByPrimaryKey()) XCTAssertEqual(rows, [ ["id": 1, "name": "Arthur", "score":1000], ]) XCTAssertEqual(player.id, 1) XCTAssertEqual(upsertedPlayer.id, 1) XCTAssertEqual(upsertedPlayer.name, "Arthur") XCTAssertEqual(upsertedPlayer.score, 1000) XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(player.callbacks.didUpdateCount, 0) XCTAssertEqual(player.callbacks.willSaveCount, 1) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(player.callbacks.didSaveCount, 1) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } do { sqlQueries.removeAll() var player = FullPlayer(id: 1, name: "Barbara", score: 100) let upsertedPlayer = try player.upsertAndFetch(db) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name", "score") \ VALUES (1,'Barbara',100) \ ON CONFLICT DO UPDATE SET "name" = "excluded"."name", "score" = "excluded"."score" \ RETURNING *, "rowid" """), sqlQueries.joined(separator: "\n")) // Test database state let rows = try Row.fetchAll(db, FullPlayer.orderByPrimaryKey()) XCTAssertEqual(rows, [ ["id": 1, "name": "Barbara", "score":100], ]) XCTAssertEqual(player.id, 1) XCTAssertEqual(upsertedPlayer.id, 1) XCTAssertEqual(upsertedPlayer.name, "Barbara") XCTAssertEqual(upsertedPlayer.score, 100) XCTAssertEqual(player.callbacks.willInsertCount, 1) XCTAssertEqual(player.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(player.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(player.callbacks.didInsertCount, 1) XCTAssertEqual(player.callbacks.willUpdateCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(player.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(player.callbacks.didUpdateCount, 0) XCTAssertEqual(player.callbacks.willSaveCount, 1) XCTAssertEqual(player.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(player.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(player.callbacks.didSaveCount, 1) XCTAssertEqual(player.callbacks.willDeleteCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(player.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(player.callbacks.didDeleteCount, 0) } } } func test_upsertAndFetch_as() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { throw XCTSkip("UPSERT is not available") } #else guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { throw XCTSkip("UPSERT is not available") } #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try partialPlayer.upsertAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") \ VALUES (NULL,'Arthur') \ ON CONFLICT DO UPDATE SET "name" = "excluded"."name" \ RETURNING *, "rowid" """), sqlQueries.joined(separator: "\n")) XCTAssertEqual(partialPlayer.id, 1) XCTAssertEqual(fullPlayer.id, 1) XCTAssertEqual(fullPlayer.name, "Arthur") XCTAssertEqual(fullPlayer.score, 1000) XCTAssertEqual(partialPlayer.callbacks.willInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundInsertExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didInsertCount, 1) XCTAssertEqual(partialPlayer.callbacks.willUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundUpdateExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didUpdateCount, 0) XCTAssertEqual(partialPlayer.callbacks.willSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveEnterCount, 1) XCTAssertEqual(partialPlayer.callbacks.aroundSaveExitCount, 1) XCTAssertEqual(partialPlayer.callbacks.didSaveCount, 1) XCTAssertEqual(partialPlayer.callbacks.willDeleteCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteEnterCount, 0) XCTAssertEqual(partialPlayer.callbacks.aroundDeleteExitCount, 0) XCTAssertEqual(partialPlayer.callbacks.didDeleteCount, 0) } } } } // MARK: - Callback Misuse extension MutablePersistableRecordTests { func test_aroundSave_misuse_by_not_calling_the_action() throws { struct BadRecord: MutablePersistableRecord, Encodable { let id = 1 func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws { // It is a programmer error to not call the `save` argument } } do { try makeDatabaseQueue().write { db in try db.execute(sql: "CREATE TABLE badRecord(a)") try BadRecord().update(db) XCTFail("Expected SQLITE_MISUSE error") } } catch DatabaseError.SQLITE_MISUSE { } } func test_aroundSave_misuse_by_not_rethrowing_the_action_error() throws { struct BadRecord: MutablePersistableRecord, Encodable { let id = 1 func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws { // It is a programmer error to not rethrow the error of the `save` argument _ = try? save() } } do { try makeDatabaseQueue().write { db in try db.execute(sql: "CREATE TABLE badRecord(a)") // Fails because we can't update anything try BadRecord().update(db) XCTFail("Expected SQLITE_MISUSE error") } } catch DatabaseError.SQLITE_MISUSE { } } func test_aroundUpdate_misuse_by_not_calling_the_action() throws { struct BadRecord: MutablePersistableRecord, Encodable { let id = 1 func aroundUpdate(_ db: Database, columns: Set, update: () throws -> PersistenceSuccess) throws { // It is a programmer error to not call the `update` argument } } do { try makeDatabaseQueue().write { db in try db.execute(sql: "CREATE TABLE badRecord(a)") try BadRecord().update(db) XCTFail("Expected SQLITE_MISUSE error") } } catch DatabaseError.SQLITE_MISUSE { } } func test_aroundUpdate_misuse_by_not_rethrowing_the_action_error() throws { struct BadRecord: MutablePersistableRecord, Encodable { let id = 1 func aroundUpdate(_ db: Database, columns: Set, update: () throws -> PersistenceSuccess) throws { // It is a programmer error to not rethrow the error of the `update` argument _ = try? update() } } do { try makeDatabaseQueue().write { db in try db.execute(sql: "CREATE TABLE badRecord(a)") // Fails because we can't update anything try BadRecord().update(db) XCTFail("Expected SQLITE_MISUSE error") } } catch DatabaseError.SQLITE_MISUSE { } } func test_aroundInsert_misuse_by_not_calling_the_action() throws { struct BadRecord: MutablePersistableRecord, Encodable { let id = 1 func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws { // It is a programmer error to not call the `insert` argument } } do { try makeDatabaseQueue().write { db in try db.execute(sql: "CREATE TABLE badRecord(a)") var record = BadRecord() try record.insert(db) XCTFail("Expected SQLITE_MISUSE error") } } catch DatabaseError.SQLITE_MISUSE { } } func test_aroundInsert_misuse_by_not_rethrowing_the_action_error() throws { struct BadRecord: MutablePersistableRecord, Encodable { let id = 1 func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws { // It is a programmer error to not rethrow the error of the `insert` argument _ = try? insert() } } do { try makeDatabaseQueue().write { db in try db.execute(sql: """ CREATE TABLE badRecord(id INTEGER PRIMARY KEY); INSERT INTO badRecord (id) VALUES (1); """) var record = BadRecord() // Fails because we insert a conflict try record.insert(db) XCTFail("Expected SQLITE_MISUSE error") } } catch DatabaseError.SQLITE_MISUSE { } } func test_aroundDelete_misuse_by_not_calling_the_action() throws { struct BadRecord: MutablePersistableRecord, Encodable { let id = 1 func aroundDelete(_ db: Database, delete: () throws -> Bool) throws { // It is a programmer error to not call the `delete` argument } } do { try makeDatabaseQueue().write { db in try db.execute(sql: "CREATE TABLE badRecord(a)") try BadRecord().delete(db) XCTFail("Expected SQLITE_MISUSE error") } } catch DatabaseError.SQLITE_MISUSE { } } func test_aroundDelete_misuse_by_not_rethrowing_the_action_error() throws { struct BadRecord: MutablePersistableRecord, Encodable { let id = 1 func aroundDelete(_ db: Database, delete: () throws -> Bool) throws { // It is a programmer error to not rethrow the error of the `delete` argument _ = try? delete() } } do { try makeDatabaseQueue().write { db in try db.execute(sql: """ CREATE TABLE badRecord(id INTEGER PRIMARY KEY); CREATE TABLE child(id INTEGER PRIMARY KEY REFERENCES badRecord(id) ON DELETE RESTRICT); INSERT INTO badRecord (id) VALUES (1); INSERT INTO child (id) VALUES (1); """) // Fails because this deletion violates a constraint try BadRecord().delete(db) XCTFail("Expected SQLITE_MISUSE error") } } catch DatabaseError.SQLITE_MISUSE { } } }