import XCTest import GRDB private struct PersistableRecordPerson : PersistableRecord { var name: String? var age: Int? static let databaseTableName = "persons" func encode(to container: inout PersistenceContainer) { container["name"] = name container["age"] = age } } private class PersistableRecordPersonClass : PersistableRecord { var id: Int64? var name: String? var age: Int? init(id: Int64?, name: String?, age: Int?) { self.id = id self.name = name self.age = age } static let databaseTableName = "persons" func encode(to container: inout PersistenceContainer) { // mangle case container["ID"] = id container["naME"] = name container["Age"] = age } func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID } } private struct PersistableRecordCountry : PersistableRecord { var isoCode: String var name: String static let databaseTableName = "countries" func encode(to container: inout PersistenceContainer) { container["isoCode"] = isoCode container["name"] = name } } 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 PersistableRecordCustomizedCountry : PersistableRecord { 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 } 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 } func didInsert(_ inserted: InsertionSuccess) { callbacks.didInsertCount += 1 } 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 Citizenship : PersistableRecord { let personID: Int64 let countryIsoCode: String static let databaseTableName = "citizenships" func encode(to container: inout PersistenceContainer) { container["countryIsoCode"] = countryIsoCode container["personID"] = personID } } private struct PartialPlayer: Codable, PersistableRecord, FetchableRecord { static let databaseTableName = "player" let callbacks = Callbacks() var id: Int64? var name: String enum CodingKeys: String, CodingKey { case id, name } func willInsert(_ db: Database) throws { callbacks.willInsertCount += 1 } func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws { callbacks.aroundInsertEnterCount += 1 _ = try insert() callbacks.aroundInsertExitCount += 1 } func didInsert(_ inserted: InsertionSuccess) { 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, PersistableRecord, 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() func willInsert(_ db: Database) throws { callbacks.willInsertCount += 1 } func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws { callbacks.aroundInsertEnterCount += 1 _ = try insert() callbacks.aroundInsertExitCount += 1 } func didInsert(_ inserted: InsertionSuccess) { 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 PersistableRecordTests: 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 TEXT NOT NULL, age INT NOT NULL); CREATE TABLE countries ( isoCode TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL); CREATE TABLE citizenships ( countryIsoCode TEXT NOT NULL REFERENCES countries(isoCode), personID INTEGER NOT NULL REFERENCES persons(id)); 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: - PersistableRecordPerson func testInsertPersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person = PersistableRecordPerson(name: "Arthur", age: 42) try person.insert(db) let rows = try Row.fetchAll(db, sql: "SELECT * FROM persons") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["name"] as String, "Arthur") } } func testSavePersistableRecordPerson() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person = PersistableRecordPerson(name: "Arthur", age: 42) try person.save(db) let rows = try Row.fetchAll(db, sql: "SELECT * FROM persons") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["name"] as String, "Arthur") } } // MARK: - PersistableRecordPersonClass func testInsertPersistableRecordPersonClass() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person = PersistableRecordPersonClass(id: nil, name: "Arthur", age: 42) 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 testUpdatePersistableRecordPersonClass() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person1 = PersistableRecordPersonClass(id: nil, name: "Arthur", age: 42) try person1.insert(db) let person2 = PersistableRecordPersonClass(id: nil, name: "Barbara", age: 39) try person2.insert(db) person1.name = "Craig" try person1.update(db) XCTAssertTrue([ "UPDATE \"persons\" SET \"age\"=42, \"name\"='Craig' WHERE \"id\"=1", "UPDATE \"persons\" SET \"name\"='Craig', \"age\"=42 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 testPartialUpdatePersistableRecordPersonClass() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person1 = PersistableRecordPersonClass(id: nil, name: "Arthur", age: 24) try person1.insert(db) let person2 = PersistableRecordPersonClass(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 testSavePersistableRecordPersonClass() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person1 = PersistableRecordPersonClass(id: nil, name: "Arthur", age: 42) 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") let person2 = PersistableRecordPersonClass(id: nil, name: "Barbara", age: 39) 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 testDeletePersistableRecordPersonClass() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person1 = PersistableRecordPersonClass(id: nil, name: "Arthur", age: 42) try person1.insert(db) let person2 = PersistableRecordPersonClass(id: nil, name: "Barbara", age: 39) 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 testExistsPersistableRecordPersonClass() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person = PersistableRecordPersonClass(id: nil, name: "Arthur", age: 42) try person.insert(db) XCTAssertTrue(try person.exists(db)) try person.delete(db) XCTAssertFalse(try person.exists(db)) } } // MARK: - PersistableRecordCountry func testInsertPersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let country = PersistableRecordCountry(isoCode: "FR", name: "France") try country.insert(db) let rows = try Row.fetchAll(db, sql: "SELECT * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France") } } func testUpdatePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = PersistableRecordCountry(isoCode: "FR", name: "France") try country1.insert(db) let country2 = PersistableRecordCountry(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 * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["isoCode"] as String, "US") XCTAssertEqual(rows[1]["name"] as String, "United States") } } func testPartialUpdatePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = PersistableRecordCountry(isoCode: "FR", name: "France") try country1.insert(db) let country2 = PersistableRecordCountry(isoCode: "US", name: "United States") try country2.insert(db) do { country1.name = "France Métropolitaine" try country1.update(db, columns: [String]()) XCTAssertEqual(self.lastSQLQuery, "UPDATE \"countries\" SET \"isoCode\"='FR' WHERE \"isoCode\"='FR'") let rows = try Row.fetchAll(db, sql: "SELECT * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France") XCTAssertEqual(rows[1]["isoCode"] as String, "US") XCTAssertEqual(rows[1]["name"] as String, "United States") } do { country1.name = "France Métropolitaine" try country1.update(db, columns: [Column("name")]) XCTAssertEqual(self.lastSQLQuery, "UPDATE \"countries\" SET \"name\"='France Métropolitaine' WHERE \"isoCode\"='FR'") let rows = try Row.fetchAll(db, sql: "SELECT * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["isoCode"] as String, "US") XCTAssertEqual(rows[1]["name"] as String, "United States") } } } func testSavePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = PersistableRecordCountry(isoCode: "FR", name: "France") try country1.save(db) var rows = try Row.fetchAll(db, sql: "SELECT * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France") let country2 = PersistableRecordCountry(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 * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["isoCode"] as String, "US") XCTAssertEqual(rows[1]["name"] as String, "United States") try country1.delete(db) try country1.save(db) rows = try Row.fetchAll(db, sql: "SELECT * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["isoCode"] as String, "US") XCTAssertEqual(rows[1]["name"] as String, "United States") } } func testDeletePersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let country1 = PersistableRecordCountry(isoCode: "FR", name: "France") try country1.insert(db) let country2 = PersistableRecordCountry(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 * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["isoCode"] as String, "US") XCTAssertEqual(rows[0]["name"] as String, "United States") } } func testExistsPersistableRecordCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let country = PersistableRecordCountry(isoCode: "FR", name: "France") try country.insert(db) XCTAssertTrue(try country.exists(db)) try country.delete(db) XCTAssertFalse(try country.exists(db)) } } // MARK: - PersistableRecordCustomizedCountry func testInsertPersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let country = PersistableRecordCustomizedCountry( 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 * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France") } } func testUpdatePersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = PersistableRecordCustomizedCountry( isoCode: "FR", name: "France") try country1.insert(db) let country2 = PersistableRecordCustomizedCountry( 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 * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["isoCode"] as String, "US") XCTAssertEqual(rows[1]["name"] as String, "United States") } } func testRecordErrorPersistableRecordCustomizedCountry() throws { let country = PersistableRecordCustomizedCountry( 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 testPartialUpdatePersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = PersistableRecordCustomizedCountry( isoCode: "FR", name: "France") try country1.insert(db) let country2 = PersistableRecordCustomizedCountry( isoCode: "US", name: "United States") try country2.insert(db) country1.name = "France Métropolitaine" try country1.update(db, columns: ["name"]) 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 * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["isoCode"] as String, "US") XCTAssertEqual(rows[1]["name"] as String, "United States") } } func testSavePersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var country1 = PersistableRecordCustomizedCountry( 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 * FROM countries") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France") let country2 = PersistableRecordCustomizedCountry( 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 * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["isoCode"] as String, "US") 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 * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 2) XCTAssertEqual(rows[0]["isoCode"] as String, "FR") XCTAssertEqual(rows[0]["name"] as String, "France Métropolitaine") XCTAssertEqual(rows[1]["isoCode"] as String, "US") XCTAssertEqual(rows[1]["name"] as String, "United States") } } func testDeletePersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let country1 = PersistableRecordCustomizedCountry( isoCode: "FR", name: "France") try country1.insert(db) let country2 = PersistableRecordCustomizedCountry( 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 * FROM countries ORDER BY isoCode") XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0]["isoCode"] as String, "US") XCTAssertEqual(rows[0]["name"] as String, "United States") } } func testExistsPersistableRecordCustomizedCountry() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let country = PersistableRecordCustomizedCountry( 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: - Errors func testInsertErrorDoesNotPreventSubsequentInserts() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let person = PersistableRecordPersonClass(id: nil, name: "Arthur", age: 12) try person.insert(db) try PersistableRecordCountry(isoCode: "FR", name: "France").insert(db) do { try Citizenship(personID: person.id!, countryIsoCode: "US").insert(db) } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT) } try Citizenship(personID: person.id!, countryIsoCode: "FR").insert(db) } } } // MARK: - Custom nested Codable types - nested saved as JSON extension PersistableRecordTests { func testOptionalNestedStruct() throws { struct NestedStruct : Codable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, Codable { static let databaseTableName = "t1" let nested: NestedStruct? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") let value = StructWithNestedType(nested: nested) try value.insert(db) let dbValue = try DatabaseValue.fetchOne(db, sql: "SELECT nested FROM t1")! // Encodable has a default implementation which encodes a model to JSON as String. // We expect here JSON in the form of a String XCTAssert(dbValue.storage.value is String) let string = dbValue.storage.value as! String if let data = string.data(using: .utf8) { do { let decoded = try JSONDecoder().decode(NestedStruct.self, from: data) XCTAssertEqual(nested.firstName, decoded.firstName) XCTAssertEqual(nested.lastName, decoded.lastName) } catch { XCTFail(error.localizedDescription) } } else { XCTFail("Failed to convert " + string) } } } func testOptionalNestedStructNil() throws { struct NestedStruct : Encodable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, Encodable { static let databaseTableName = "t1" let nested: NestedStruct? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let value = StructWithNestedType(nested: nil) try value.insert(db) let dbValue = try DatabaseValue.fetchOne(db, sql: "SELECT nested FROM t1")! // We expect here nil XCTAssertNil(dbValue.storage.value) } } func testOptionalNestedArrayStruct() throws { struct NestedStruct : Codable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, Codable { static let databaseTableName = "t1" let nested: [NestedStruct]? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") let value = StructWithNestedType(nested: [nested, nested]) try value.insert(db) let dbValue = try DatabaseValue.fetchOne(db, sql: "SELECT nested FROM t1")! // Encodable has a default implementation which encodes a model to JSON as String. // We expect here JSON in the form of a String XCTAssert(dbValue.storage.value is String) let string = dbValue.storage.value as! String if let data = string.data(using: .utf8) { do { let decoded = try JSONDecoder().decode([NestedStruct].self, from: data) XCTAssertEqual(decoded.count, 2) XCTAssertEqual(nested.firstName, decoded.first!.firstName) XCTAssertEqual(nested.lastName, decoded.first!.lastName) XCTAssertEqual(nested.firstName, decoded.last!.firstName) XCTAssertEqual(nested.lastName, decoded.last!.lastName) } catch { XCTFail(error.localizedDescription) } } else { XCTFail("Failed to convert " + string) } } } func testOptionalNestedArrayStructNil() throws { struct NestedStruct : Encodable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, Encodable { static let databaseTableName = "t1" let nested: [NestedStruct]? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let value = StructWithNestedType(nested: nil) try value.insert(db) let dbValue = try DatabaseValue.fetchOne(db, sql: "SELECT nested FROM t1")! // We expect here nil XCTAssertNil(dbValue.storage.value) } } func testNonOptionalNestedStruct() throws { struct NestedStruct : Codable { let firstName: String let lastName: String } struct StructWithNestedType : PersistableRecord, Codable { static let databaseTableName = "t1" let nested: NestedStruct } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") let value = StructWithNestedType(nested: nested) try value.insert(db) let dbValue = try DatabaseValue.fetchOne(db, sql: "SELECT nested FROM t1")! // Encodable has a default implementation which encodes a model to JSON as String. // We expect here JSON in the form of a String XCTAssert(dbValue.storage.value is String) let string = dbValue.storage.value as! String if let data = string.data(using: .utf8) { do { let decoded = try JSONDecoder().decode(NestedStruct.self, from: data) XCTAssertEqual(nested.firstName, decoded.firstName) XCTAssertEqual(nested.lastName, decoded.lastName) } catch { XCTFail(error.localizedDescription) } } else { XCTFail("Failed to convert " + string) } } } func testNonOptionalNestedArrayStruct() throws { struct NestedStruct : Codable { let firstName: String let lastName: String } struct StructWithNestedType : PersistableRecord, Codable { static let databaseTableName = "t1" let nested: [NestedStruct] } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") let value = StructWithNestedType(nested: [nested]) try value.insert(db) let dbValue = try DatabaseValue.fetchOne(db, sql: "SELECT nested FROM t1")! // Encodable has a default implementation which encodes a model to JSON as String. // We expect here JSON in the form of a String XCTAssert(dbValue.storage.value is String) let string = dbValue.storage.value as! String if let data = string.data(using: .utf8) { do { let decoded = try JSONDecoder().decode([NestedStruct].self, from: data) XCTAssertEqual(nested.firstName, decoded.first!.firstName) XCTAssertEqual(nested.lastName, decoded.first!.lastName) } catch { XCTFail(error.localizedDescription) } } else { XCTFail("Failed to convert " + string) } } } func testStringStoredInArray() throws { struct TestStruct : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let numbers: [String] } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("numbers", .text) } let model = TestStruct(numbers: ["test1", "test2", "test3"]) try model.insert(db) // Encodable has a default implementation which encodes a model to JSON as String. // We expect here JSON in the form of a String guard let fetchModel = try TestStruct.fetchOne(db) else { XCTFail("Could not find record in db") return } XCTAssertEqual(model.numbers, fetchModel.numbers) } } func testOptionalStringStoredInArray() throws { struct TestStruct : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let numbers: [String]? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("numbers", .text) } let model = TestStruct(numbers: ["test1", "test2", "test3"]) try model.insert(db) // Encodable has a default implementation which encodes a model to JSON as String. // We expect here JSON in the form of a String guard let fetchModel = try TestStruct.fetchOne(db) else { XCTFail("Could not find record in db") return } XCTAssertEqual(model.numbers, fetchModel.numbers) } } } // MARK: - Insert and Fetch extension PersistableRecordTests { 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() let 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(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() let 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(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: - Save and Fetch extension PersistableRecordTests { 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() let 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(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 { let 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(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() let 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(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() let 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(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 { let 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(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() let 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(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: - Upsert extension PersistableRecordTests { 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 { let 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 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) let 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 conflict on id do { let 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 conflict on both id and name (same row) do { let 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 conflict on both id and name (different rows) do { let 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, PersistableRecord, FetchableRecord { var word: String var kind: String var isTainted: Bool var count: Int? func encode(to container: inout PersistenceContainer) { // Don't encode count container["word"] = word container["kind"] = kind container["isTainted"] = isTainted } } // One column with specific assignment (count) // One column with no assignment (isTainted) // One column with default overwrite assignment (kind) do { let 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 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 { let 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 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, PersistableRecord, FetchableRecord { var name: String var phonenumber: String } let 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() let 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() let 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() let 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(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) } } } }