// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB import Quick import Nimble @testable import SessionUtilitiesKit class PersistableRecordUtilitiesSpec: QuickSpec { override class func spec() { // MARK: Configuration @TestState var customWriter: DatabaseQueue! = try! DatabaseQueue() @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: customWriter, migrationTargets: [ TestTarget.self ] ) // MARK: - a PersistableRecord describe("a PersistableRecord") { // MARK: -- before running the add column migration context("before running the add column migration") { // MARK: ---- fails when using the standard insert it("fails when using the standard insert") { mockStorage.write { db in expect { try TestType(columnA: "Test1", columnB: "Test1B").insert(db) } .to(throwError()) } } // MARK: ---- fails when using the standard inserted it("fails when using the standard inserted") { mockStorage.write { db in expect { try MutableTestType(columnA: "Test2", columnB: "Test2B").inserted(db) } .to(throwError()) } } // MARK: ---- fails when using the standard save and the item does not already exist it("fails when using the standard save and the item does not already exist") { mockStorage.write { db in expect { try TestType(columnA: "Test3", columnB: "Test3B").save(db) } .to(throwError()) } } // MARK: ---- fails when using the standard saved and the item does not already exist it("fails when using the standard saved and the item does not already exist") { mockStorage.write { db in expect { try MutableTestType(columnA: "Test4", columnB: "Test4B").saved(db) } .to(throwError()) } } // MARK: ---- fails when using the standard upsert and the item does not already exist it("fails when using the standard upsert and the item does not already exist") { mockStorage.write { db in expect { try TestType(columnA: "Test5", columnB: "Test5B").upsert(db) } .to(throwError()) } } // MARK: ---- fails when using the standard mutable upsert and the item does not already exist it("fails when using the standard mutable upsert and the item does not already exist") { mockStorage.write { db in expect { var result = MutableTestType(columnA: "Test6", columnB: "Test6B") try result.upsert(db) return result } .to(throwError()) } } // MARK: ---- fails when using the standard upsert and the item already exists it("fails when using the standard upsert and the item already exists") { mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO TestType (columnA) VALUES (?)", arguments: StatementArguments(["Test19"]) ) try TestType(columnA: "Test19", columnB: "Test19B").upsert(db) } .to(throwError()) } } // MARK: ---- fails when using the standard mutable upsert and the item already exists it("fails when using the standard mutable upsert and the item already exists") { mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test20"]) ) var result = MutableTestType(id: 1, columnA: "Test20", columnB: "Test20B") try result.upsert(db) return result } .to(throwError()) } } // MARK: ---- succeeds when using the migration safe insert it("succeeds when using the migration safe insert") { mockStorage.write { db in expect { try TestType(columnA: "Test7", columnB: "Test7B").migrationSafeInsert(db) } .toNot(throwError()) } mockStorage.read { db in expect(try TestType.fetchAll(db)) .toNot(beNil()) } } // MARK: ---- succeeds when using the migration safe inserted it("succeeds when using the migration safe inserted") { mockStorage.write { db in expect { try MutableTestType(columnA: "Test8", columnB: "Test8B").migrationSafeInserted(db) } .toNot(throwError()) expect { try MutableTestType(columnA: "Test9", columnB: "Test9B") .migrationSafeInserted(db) .id } .toNot(beNil()) } mockStorage.read { db in expect(try MutableTestType.fetchAll(db)) .toNot(beNil()) } } // MARK: ---- succeeds when using the migration safe save and the item does not already exist it("succeeds when using the migration safe save and the item does not already exist") { mockStorage.write { db in expect { try TestType(columnA: "Test10", columnB: "Test10B").migrationSafeSave(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the migration safe saved and the item does not already exist it("succeeds when using the migration safe saved and the item does not already exist") { mockStorage.write { db in expect { try MutableTestType(columnA: "Test11", columnB: "Test11B").migrationSafeSaved(db) } .toNot(throwError()) expect { try MutableTestType(columnA: "Test12", columnB: "Test12B") .migrationSafeSaved(db) .id } .toNot(beNil()) } mockStorage.read { db in expect(try MutableTestType.fetchAll(db)) .toNot(beNil()) } } // MARK: ---- succeeds when using the migration safe upsert and the item does not already exist it("succeeds when using the migration safe upsert and the item does not already exist") { mockStorage.write { db in expect { try TestType(columnA: "Test13", columnB: "Test13B").migrationSafeUpsert(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the migration safe mutable upsert and the item does not already exist it("succeeds when using the migration safe mutable upsert and the item does not already exist") { mockStorage.write { db in expect { let result = MutableTestType(columnA: "Test14", columnB: "Test14B") try result.migrationSafeUpsert(db) return result } .toNot(throwError()) } mockStorage.read { db in expect(try MutableTestType.fetchAll(db)) .toNot(beNil()) } } // MARK: ---- succeeds when using the standard save and the item already exists it("succeeds when using the standard save and the item already exists") { /// **Note:** The built-in 'update' method only updates existing columns so this shouldn't fail mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO TestType (columnA) VALUES (?)", arguments: StatementArguments(["Test16"]) ) try TestType(columnA: "Test16", columnB: "Test16B").save(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the standard saved and the item already exists it("succeeds when using the standard saved and the item already exists") { /// **Note:** The built-in 'update' method only updates existing columns so this won't fail /// due to the structure discrepancy but won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test17"]) ) _ = try MutableTestType(id: 1, columnA: "Test17", columnB: "Test17B").saved(db) } .toNot(throwError()) expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test18"]) ) return try MutableTestType(id: 2, columnA: "Test18", columnB: "Test18B") .saved(db) .id } .toNot(beNil()) } mockStorage.read { db in let types: [MutableTestType]? = try MutableTestType.fetchAll(db) expect(types).toNot(beNil()) expect(types?.compactMap { $0.id }.count).to(equal(types?.count)) } } } // MARK: -- after running the add column migration context("after running the add column migration") { beforeEach { var migrator: DatabaseMigrator = DatabaseMigrator() migrator.registerMigration( mockStorage, targetIdentifier: TestAddColumnMigration.target, migration: TestAddColumnMigration.self ) expect { try migrator.migrate(customWriter) } .toNot(throwError()) } // MARK: ---- succeeds when using the standard insert it("succeeds when using the standard insert") { mockStorage.write { db in expect { try TestType(columnA: "Test1", columnB: "Test1B").insert(db) } .toNot(throwError()) } mockStorage.read { db in expect(try TestType.fetchAll(db)) .toNot(beNil()) } } // MARK: ---- succeeds when using the standard inserted it("succeeds when using the standard inserted") { mockStorage.write { db in expect { try MutableTestType(columnA: "Test2", columnB: "Test2B").inserted(db) } .toNot(throwError()) } mockStorage.read { db in expect(try MutableTestType.fetchAll(db)) .toNot(beNil()) } } // MARK: ---- succeeds when using the standard save and the item does not already exist it("succeeds when using the standard save and the item does not already exist") { mockStorage.write { db in expect { try TestType(columnA: "Test3", columnB: "Test3B").save(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the standard saved and the item does not already exist it("succeeds when using the standard saved and the item does not already exist") { mockStorage.write { db in expect { try MutableTestType(columnA: "Test3", columnB: "Test3B").saved(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the standard save and the item already exists it("succeeds when using the standard save and the item already exists") { mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO TestType (columnA) VALUES (?)", arguments: StatementArguments(["Test4"]) ) try TestType(columnA: "Test4", columnB: "Test4B").save(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the standard saved and the item already exists it("succeeds when using the standard saved and the item already exists") { /// **Note:** The built-in 'update' method won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test5"]) ) _ = try MutableTestType(id: 1, columnA: "Test5", columnB: "Test5B").saved(db) } .toNot(throwError()) expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test6"]) ) return try MutableTestType(id: 2, columnA: "Test6", columnB: "Test6B") .saved(db) .id } .toNot(beNil()) } mockStorage.read { db in let types: [MutableTestType]? = try MutableTestType.fetchAll(db) expect(types).toNot(beNil()) expect(types?.compactMap { $0.id }.count).to(equal(types?.count)) } } // MARK: ---- succeeds when using the standard upsert and the item does not already exist it("succeeds when using the standard upsert and the item does not already exist") { mockStorage.write { db in expect { try TestType(columnA: "Test7", columnB: "Test7B").upsert(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the standard mutable upsert and the item does not already exist it("succeeds when using the standard mutable upsert and the item does not already exist") { mockStorage.write { db in expect { var result = MutableTestType(columnA: "Test8", columnB: "Test8B") try result.upsert(db) return result } .toNot(throwError()) } } // MARK: ---- succeeds when using the standard upsert and the item already exists it("succeeds when using the standard upsert and the item already exists") { mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO TestType (columnA) VALUES (?)", arguments: StatementArguments(["Test9"]) ) try TestType(columnA: "Test9", columnB: "Test9B").upsert(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the standard mutable upsert and the item already exists it("succeeds when using the standard mutable upsert and the item already exists") { /// **Note:** The built-in 'update' method won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test10"]) ) var result = MutableTestType(id: 1, columnA: "Test10", columnB: "Test10B") try result.upsert(db) return result } .toNot(throwError()) expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test11"]) ) var result = MutableTestType(id: 2, columnA: "Test11", columnB: "Test11B") try result.upsert(db) return result.id } .toNot(beNil()) } mockStorage.read { db in let types: [MutableTestType]? = try MutableTestType.fetchAll(db) expect(types).toNot(beNil()) expect(types?.compactMap { $0.id }.count).to(equal(types?.count)) } } // MARK: ---- succeeds when using the migration safe insert it("succeeds when using the migration safe insert") { mockStorage.write { db in expect { try TestType(columnA: "Test12", columnB: "Test12B").migrationSafeInsert(db) } .toNot(throwError()) } mockStorage.read { db in expect(try TestType.fetchAll(db)) .toNot(beNil()) } } // MARK: ---- succeeds when using the migration safe inserted it("succeeds when using the migration safe inserted") { mockStorage.write { db in expect { try MutableTestType(columnA: "Test13", columnB: "Test13B").migrationSafeInserted(db) } .toNot(throwError()) expect { try MutableTestType(columnA: "Test14", columnB: "Test14B") .migrationSafeInserted(db) .id } .toNot(beNil()) } mockStorage.read { db in expect(try MutableTestType.fetchAll(db)) .toNot(beNil()) } } // MARK: ---- succeeds when using the migration safe save and the item does not already exist it("succeeds when using the migration safe save and the item does not already exist") { mockStorage.write { db in expect { try TestType(columnA: "Test15", columnB: "Test15B").migrationSafeSave(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the migration safe saved and the item does not already exist it("succeeds when using the migration safe saved and the item does not already exist") { mockStorage.write { db in expect { try MutableTestType(columnA: "Test16", columnB: "Test16B").migrationSafeSaved(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the migration safe save and the item already exists it("succeeds when using the migration safe save and the item already exists") { mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO TestType (columnA) VALUES (?)", arguments: StatementArguments(["Test17"]) ) try TestType(columnA: "Test17", columnB: "Test17B").migrationSafeSave(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the migration safe saved and the item already exists it("succeeds when using the migration safe saved and the item already exists") { /// **Note:** The built-in 'update' method won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test18"]) ) _ = try MutableTestType(id: 1, columnA: "Test18", columnB: "Test18B") .migrationSafeSaved(db) } .toNot(throwError()) expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test19"]) ) return try MutableTestType(id: 2, columnA: "Test19", columnB: "Test19B") .migrationSafeSaved(db) .id } .toNot(beNil()) } mockStorage.read { db in let types: [MutableTestType]? = try MutableTestType.fetchAll(db) expect(types).toNot(beNil()) expect(types?.compactMap { $0.id }.count).to(equal(types?.count)) } } // MARK: ---- succeeds when using the migration safe upsert and the item does not already exist it("succeeds when using the migration safe upsert and the item does not already exist") { mockStorage.write { db in expect { try TestType(columnA: "Test20", columnB: "Test20B").migrationSafeUpsert(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the migration safe mutable upsert and the item does not already exist it("succeeds when using the migration safe mutable upsert and the item does not already exist") { mockStorage.write { db in expect { let result = MutableTestType(columnA: "Test21", columnB: "Test21B") try result.migrationSafeUpsert(db) return result } .toNot(throwError()) } } // MARK: ---- succeeds when using the migration safe upsert and the item already exists it("succeeds when using the migration safe upsert and the item already exists") { mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO TestType (columnA) VALUES (?)", arguments: StatementArguments(["Test22"]) ) try TestType(columnA: "Test22", columnB: "Test22B").migrationSafeUpsert(db) } .toNot(throwError()) } } // MARK: ---- succeeds when using the migration safe mutable upsert and the item already exists it("succeeds when using the migration safe mutable upsert and the item already exists") { /// **Note:** The built-in 'update' method won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test23"]) ) let result = MutableTestType(id: 1, columnA: "Test23", columnB: "Test23B") try result.migrationSafeUpsert(db) return result } .toNot(throwError()) expect { try db.execute( sql: "INSERT INTO MutableTestType (columnA) VALUES (?)", arguments: StatementArguments(["Test24"]) ) let result = MutableTestType(id: 2, columnA: "Test24", columnB: "Test24B") try result.migrationSafeUpsert(db) return result.id } .toNot(beNil()) } mockStorage.read { db in let types: [MutableTestType]? = try MutableTestType.fetchAll(db) expect(types).toNot(beNil()) expect(types?.compactMap { $0.id }.count).to(equal(types?.count)) } } } } } } // MARK: - Test Types fileprivate struct TestType: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "TestType" } public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression { case columnA case columnB } public let columnA: String public let columnB: String? } fileprivate struct MutableTestType: Codable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "MutableTestType" } public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression { case id case columnA case columnB } public var id: Int64? public let columnA: String public let columnB: String? init(id: Int64? = nil, columnA: String, columnB: String?) { self.id = id self.columnA = columnA self.columnB = columnB } mutating func didInsert(_ inserted: InsertionSuccess) { self.id = inserted.rowID } } fileprivate enum TestInsertTestTypeMigration: Migration { static let target: TargetMigrations.Identifier = .test static let identifier: String = "TestInsertTestType" static let needsConfigSync: Bool = false static let minExpectedRunDuration: TimeInterval = 0 static let fetchedTables: [(TableRecord & FetchableRecord).Type] = [] static let createdOrAlteredTables: [(FetchableRecord & TableRecord).Type] = [TestType.self, MutableTestType.self] static func migrate(_ db: Database) throws { try db.create(table: TestType.self) { t in t.column(.columnA, .text).primaryKey() } try db.create(table: MutableTestType.self) { t in t.column(.id, .integer).primaryKey(autoincrement: true) t.column(.columnA, .text).unique() } } } fileprivate enum TestAddColumnMigration: Migration { static let target: TargetMigrations.Identifier = .test static let identifier: String = "TestAddColumn" static let needsConfigSync: Bool = false static let minExpectedRunDuration: TimeInterval = 0 static let fetchedTables: [(TableRecord & FetchableRecord).Type] = [] static let createdOrAlteredTables: [(FetchableRecord & TableRecord).Type] = [TestType.self, MutableTestType.self] static func migrate(_ db: Database) throws { try db.alter(table: TestType.self) { t in t.add(.columnB, .text) } try db.alter(table: MutableTestType.self) { t in t.add(.columnB, .text) } } } fileprivate struct TestTarget: MigratableTarget { static func migrations() -> TargetMigrations { return TargetMigrations( identifier: .test, migrations: (0..<100) .map { _ in [] } .appending([TestInsertTestTypeMigration.self]) ) } }