mirror of https://github.com/oxen-io/session-ios
Added a method to allow safer database inserts
Fixed an issue where the app settings were updating immediately making them seem to Updated GRDB to version 6.1 and SQLCipher to 4.5.2 Added a method which allows for inserting into the database while omitting columns which exist in the object but not in the database (so allow for old migrations to run with less issues) Updated all the migrations to use the migration safe insert method Removed some ObjC support extension functionspull/718/head
parent
6c58e08b4c
commit
89df1261e3
@ -0,0 +1,277 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
||||
// MARK: - Migration Safe Functions
|
||||
|
||||
public extension MutablePersistableRecord where Self: TableRecord & EncodableRecord & Codable {
|
||||
func migrationSafeInsert(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil
|
||||
) throws {
|
||||
var record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||
try record.insert(db, onConflict: conflictResolution)
|
||||
}
|
||||
|
||||
func migrationSafeInserted(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil
|
||||
) throws -> Self {
|
||||
let record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||
let updatedRecord = try record.inserted(db, onConflict: conflictResolution)
|
||||
return updatedRecord.originalRecord
|
||||
}
|
||||
|
||||
func migrationSafeSave(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil
|
||||
) throws {
|
||||
var record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||
try record.save(db, onConflict: conflictResolution)
|
||||
}
|
||||
|
||||
func migrationSafeSaved(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil
|
||||
) throws -> Self {
|
||||
let record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||
let updatedRecord = try record.saved(db, onConflict: conflictResolution)
|
||||
return updatedRecord.originalRecord
|
||||
}
|
||||
|
||||
func migrationSafeUpsert(_ db: Database) throws {
|
||||
var record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||
try record.upsert(db)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MigrationSafeMutableRecord
|
||||
|
||||
private class MigrationSafeRecord<T: PersistableRecord & Encodable>: MigrationSafeMutableRecord<T> {}
|
||||
|
||||
private class MigrationSafeMutableRecord<T: MutablePersistableRecord & Encodable>: MutablePersistableRecord & Encodable {
|
||||
public static var databaseTableName: String { T.databaseTableName }
|
||||
|
||||
fileprivate var originalRecord: T
|
||||
private let availableColumnNames: [String]
|
||||
|
||||
init(_ db: Database, originalRecord: T) throws {
|
||||
// Check the current columns in the database and filter out any properties on the object which
|
||||
// don't exist in the dictionary
|
||||
self.originalRecord = originalRecord
|
||||
self.availableColumnNames = try db.columns(in: Self.databaseTableName).map(\.name)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
let filteredEncoder: FilteredEncoder = FilteredEncoder(
|
||||
originalEncoder: encoder,
|
||||
availableKeys: availableColumnNames
|
||||
)
|
||||
try originalRecord.encode(to: filteredEncoder)
|
||||
}
|
||||
|
||||
// MARK: - Persistence Callbacks
|
||||
|
||||
func willInsert(_ db: Database) throws {
|
||||
try originalRecord.willInsert(db)
|
||||
}
|
||||
|
||||
func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws {
|
||||
try originalRecord.aroundInsert(db, insert: insert)
|
||||
}
|
||||
|
||||
func didInsert(_ inserted: InsertionSuccess) {
|
||||
originalRecord.didInsert(inserted)
|
||||
}
|
||||
|
||||
func willUpdate(_ db: Database, columns: Set<String>) throws {
|
||||
try originalRecord.willUpdate(db, columns: columns)
|
||||
}
|
||||
|
||||
func aroundUpdate(_ db: Database, columns: Set<String>, update: () throws -> PersistenceSuccess) throws {
|
||||
try originalRecord.aroundUpdate(db, columns: columns, update: update)
|
||||
}
|
||||
|
||||
func didUpdate(_ updated: PersistenceSuccess) {
|
||||
originalRecord.didUpdate(updated)
|
||||
}
|
||||
|
||||
func willSave(_ db: Database) throws {
|
||||
try originalRecord.willSave(db)
|
||||
}
|
||||
|
||||
func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws {
|
||||
try originalRecord.aroundSave(db, save: save)
|
||||
}
|
||||
|
||||
func didSave(_ saved: PersistenceSuccess) {
|
||||
originalRecord.didSave(saved)
|
||||
}
|
||||
|
||||
func willDelete(_ db: Database) throws {
|
||||
try originalRecord.willDelete(db)
|
||||
}
|
||||
|
||||
func aroundDelete(_ db: Database, delete: () throws -> Bool) throws {
|
||||
try originalRecord.aroundDelete(db, delete: delete)
|
||||
}
|
||||
|
||||
func didDelete(deleted: Bool) {
|
||||
originalRecord.didDelete(deleted: deleted)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FilteredEncoder
|
||||
|
||||
private class FilteredEncoder: Encoder {
|
||||
let originalEncoder: Encoder
|
||||
let availableKeys: [String]
|
||||
|
||||
init(originalEncoder: Encoder, availableKeys: [String]) {
|
||||
self.originalEncoder = originalEncoder
|
||||
self.availableKeys = availableKeys
|
||||
}
|
||||
|
||||
var codingPath: [CodingKey] { originalEncoder.codingPath }
|
||||
var userInfo: [CodingUserInfoKey: Any] { originalEncoder.userInfo }
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
|
||||
let container = originalEncoder.container(keyedBy: type)
|
||||
let filteredContainer = FilteredKeyedEncodingContainer(
|
||||
availableKeys: availableKeys,
|
||||
originalContainer: container
|
||||
)
|
||||
|
||||
return KeyedEncodingContainer(filteredContainer)
|
||||
}
|
||||
|
||||
func unkeyedContainer() -> UnkeyedEncodingContainer { originalEncoder.unkeyedContainer() }
|
||||
func singleValueContainer() -> SingleValueEncodingContainer { originalEncoder.singleValueContainer() }
|
||||
}
|
||||
|
||||
// MARK: - FilteredKeyedEncodingContainer
|
||||
|
||||
private class FilteredKeyedEncodingContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
|
||||
let codingPath: [CodingKey]
|
||||
let availableKeys: [String]
|
||||
var originalContainer: KeyedEncodingContainer<Key>
|
||||
|
||||
init(availableKeys: [String], originalContainer: KeyedEncodingContainer<Key>) {
|
||||
self.availableKeys = availableKeys
|
||||
self.codingPath = originalContainer.codingPath
|
||||
self.originalContainer = originalContainer
|
||||
}
|
||||
|
||||
func encodeNil(forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encodeNil(forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: Bool, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: String, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: Double, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: Float, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: Int, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: Int8, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: Int16, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: Int32, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: Int64, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: UInt, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: UInt8, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: UInt16, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: UInt32, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode(_ value: UInt64, forKey key: Key) throws {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable {
|
||||
guard availableKeys.contains(key.stringValue) else { return }
|
||||
|
||||
try originalContainer.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
|
||||
return originalContainer.nestedContainer(keyedBy: keyType, forKey: key)
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||
return originalContainer.nestedUnkeyedContainer(forKey: key)
|
||||
}
|
||||
|
||||
func superEncoder() -> Encoder {
|
||||
return originalContainer.superEncoder()
|
||||
}
|
||||
|
||||
func superEncoder(forKey key: Key) -> Encoder {
|
||||
return originalContainer.superEncoder(forKey: key)
|
||||
}
|
||||
}
|
@ -0,0 +1,681 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SessionUtilitiesKit
|
||||
|
||||
class PersistableRecordUtilitiesSpec: QuickSpec {
|
||||
static var customWriter: DatabaseQueue!
|
||||
|
||||
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?
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Spec
|
||||
|
||||
override func spec() {
|
||||
var customWriter: DatabaseQueue!
|
||||
var mockStorage: Storage!
|
||||
|
||||
describe("a PersistableRecord") {
|
||||
beforeEach {
|
||||
customWriter = try! DatabaseQueue()
|
||||
PersistableRecordUtilitiesSpec.customWriter = customWriter
|
||||
mockStorage = Storage(
|
||||
customWriter: customWriter,
|
||||
customMigrations: [
|
||||
TargetMigrations(
|
||||
identifier: .test,
|
||||
migrations: (0..<100)
|
||||
.map { _ in [] }
|
||||
.appending([TestInsertTestTypeMigration.self])
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
afterEach {
|
||||
customWriter = nil
|
||||
mockStorage = nil
|
||||
}
|
||||
|
||||
context("before running the add column migration") {
|
||||
it("fails when using the standard insert") {
|
||||
mockStorage.write { db in
|
||||
expect {
|
||||
try TestType(columnA: "Test1", columnB: "Test1B").insert(db)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
}
|
||||
|
||||
it("fails when using the standard inserted") {
|
||||
mockStorage.write { db in
|
||||
expect {
|
||||
try MutableTestType(columnA: "Test2", columnB: "Test2B").inserted(db)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
it("succeeds when using the migration safe mutable upsert and the item does not already exist") {
|
||||
mockStorage.write { db in
|
||||
expect {
|
||||
var 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())
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The built-in 'update' method only updates existing columns so this shouldn't fail
|
||||
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(["Test16"])
|
||||
)
|
||||
try TestType(columnA: "Test16", columnB: "Test16B").save(db)
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
it("succeeds when using the standard saved and the item already exists") {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("after running the add column migration") {
|
||||
beforeEach {
|
||||
var migrator: DatabaseMigrator = DatabaseMigrator()
|
||||
migrator.registerMigration(
|
||||
TestAddColumnMigration.target,
|
||||
migration: TestAddColumnMigration.self
|
||||
)
|
||||
|
||||
expect { try migrator.migrate(customWriter) }
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The built-in 'update' method won't update the id as that only happens on
|
||||
// insert
|
||||
it("succeeds when using the standard saved and the item already exists") {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The built-in 'update' method won't update the id as that only happens on
|
||||
// insert
|
||||
it("succeeds 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(["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))
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The built-in 'update' method won't update the id as that only happens on
|
||||
// insert
|
||||
it("succeeds when using the migration safe saved and the item already exists") {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
it("succeeds when using the migration safe mutable upsert and the item does not already exist") {
|
||||
mockStorage.write { db in
|
||||
expect {
|
||||
var result = MutableTestType(columnA: "Test21", columnB: "Test21B")
|
||||
try result.migrationSafeUpsert(db)
|
||||
return result
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The built-in 'update' method won't update the id as that only happens on
|
||||
// insert
|
||||
it("succeeds when using the migration safe mutable upsert and the item already exists") {
|
||||
mockStorage.write { db in
|
||||
expect {
|
||||
try db.execute(
|
||||
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||
arguments: StatementArguments(["Test23"])
|
||||
)
|
||||
var 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"])
|
||||
)
|
||||
var 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue