Refactor PersistenceError.recordNotFound

This commit is contained in:
Gwendal Roué 2019-02-25 20:17:49 +01:00
parent fcbbe17909
commit 63192e2b75
12 changed files with 113 additions and 50 deletions

View File

@ -20,7 +20,10 @@ public enum PersistenceError: Error, CustomStringConvertible {
/// Thrown by MutablePersistableRecord.update() when no matching row could be
/// found in the database.
case recordNotFound(MutablePersistableRecord)
///
/// - databaseTableName: the table of the unfound record
/// - key: the key of the unfound record (column and values)
case recordNotFound(databaseTableName: String, key: [String: DatabaseValue])
}
// CustomStringConvertible
@ -28,8 +31,9 @@ extension PersistenceError {
/// :nodoc:
public var description: String {
switch self {
case .recordNotFound(let record):
return "Not found: \(record)"
case let .recordNotFound(databaseTableName: databaseTableName, key: key):
let row = Row(key) // For nice output
return "Record not found in table \(databaseTableName): \(row.description)"
}
}
}
@ -662,18 +666,19 @@ extension MutablePersistableRecord {
// MARK: - CRUD Internals
/// Return true if record has a non-null primary key
/// Return a non-nil dictionary if record has a non-null primary key
@usableFromInline
func canUpdate(_ db: Database) throws -> Bool {
func primaryKey(_ db: Database) throws -> [String: DatabaseValue]? {
let databaseTableName = type(of: self).databaseTableName
let primaryKey = try db.primaryKey(databaseTableName)
let primaryKeyInfo = try db.primaryKey(databaseTableName)
let container = try PersistenceContainer(db, self)
for column in primaryKey.columns {
if let value = container[caseInsensitive: column], !value.databaseValue.isNull {
return true
}
let primaryKey = Dictionary(uniqueKeysWithValues: primaryKeyInfo.columns.map {
($0, container[caseInsensitive: $0]?.databaseValue ?? .null)
})
if primaryKey.allSatisfy({ $0.value.isNull }) {
return nil
}
return false
return primaryKey
}
/// Don't invoke this method directly: it is an internal method for types
@ -709,13 +714,14 @@ extension MutablePersistableRecord {
/// match any row in the database.
@inlinable
public func performUpdate(_ db: Database, columns: Set<String>) throws {
guard let statement = try DAO(db, self).updateStatement(columns: columns, onConflict: type(of: self).persistenceConflictPolicy.conflictResolutionForUpdate) else {
let dao = try DAO(db, self)
guard let statement = try dao.updateStatement(columns: columns, onConflict: type(of: self).persistenceConflictPolicy.conflictResolutionForUpdate) else {
// Nil primary key
throw PersistenceError.recordNotFound(self)
try dao.recordNotFound()
}
try statement.execute()
if db.changesCount == 0 {
throw PersistenceError.recordNotFound(self)
try dao.recordNotFound()
}
}
@ -734,14 +740,10 @@ extension MutablePersistableRecord {
// that override insert or save have opportunity to perform their
// custom job.
if try canUpdate(db) {
if let key = try primaryKey(db) {
do {
try update(db)
} catch PersistenceError.recordNotFound {
// TODO: check that the not persisted objet is self
//
// Why? Adopting types could override update() and update
// another object which may be the one throwing this error.
} catch PersistenceError.recordNotFound(databaseTableName: type(of: self).databaseTableName, key: key) {
try insert(db)
}
} else {
@ -1058,14 +1060,10 @@ extension PersistableRecord {
// Make sure we call self.insert and self.update so that classes that
// override insert or save have opportunity to perform their custom job.
if try canUpdate(db) {
if let key = try primaryKey(db) {
do {
try update(db)
} catch PersistenceError.recordNotFound {
// TODO: check that the not persisted objet is self
//
// Why? Adopting types could override update() and update another
// object which may be the one throwing this error.
} catch PersistenceError.recordNotFound(databaseTableName: type(of: self).databaseTableName, key: key) {
try insert(db)
}
} else {
@ -1163,9 +1161,9 @@ final class DAO<Record: MutablePersistableRecord> {
let persistenceContainer: PersistenceContainer
/// The table name
let databaseTableName: String
@usableFromInline let databaseTableName: String
/// The table primary key
/// The table primary key info
@usableFromInline let primaryKey: PrimaryKeyInfo
@usableFromInline
@ -1283,6 +1281,17 @@ final class DAO<Record: MutablePersistableRecord> {
statement.unsafeSetArguments(StatementArguments(primaryKeyValues))
return statement
}
/// Throws a PersistenceError.recordNotFound error
@usableFromInline
func recordNotFound() throws -> Never {
let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map {
($0, persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null)
})
throw PersistenceError.recordNotFound(
databaseTableName: databaseTableName,
key: key)
}
}

View File

@ -276,11 +276,11 @@ open class Record : FetchableRecord, TableRecord, PersistableRecord {
let dao = try DAO(db, self)
guard let statement = try dao.updateStatement(columns: columns, onConflict: type(of: self).persistenceConflictPolicy.conflictResolutionForUpdate) else {
// Nil primary key
throw PersistenceError.recordNotFound(self)
try dao.recordNotFound()
}
try statement.execute()
if db.changesCount == 0 {
throw PersistenceError.recordNotFound(self)
try dao.recordNotFound()
}
// Set hasDatabaseChanges to false

View File

@ -682,7 +682,7 @@ let playerId = db.lastInsertedRowID
Don't miss [Records](#records), that provide classic **persistence methods**:
```swift
let player = Player(name: "Arthur", score: 1000)
var player = Player(name: "Arthur", score: 1000)
try player.insert(db)
let playerId = player.id
```

View File

@ -24,7 +24,6 @@
- [ ] Allow joining methods on DerivableRequest
- [ ] DatabaseWriter.assertWriteAccess()
- [ ] Configuration.crashOnError = true
- [ ] Remove associated record from PersistenceError.recordNotFound. It should be possible, in userland, to write a method that throws this error without having a fully constructed record.
- [ ] Support for "INSERT INTO ... SELECT ...". For example:
```swift

View File

@ -696,4 +696,23 @@ class MutablePersistableRecordTests: GRDBTestCase {
}
}
}
func testPersistenceErrorRecordNotFoundDescription() {
do {
let error = PersistenceError.recordNotFound(
databaseTableName: "place",
key: ["id": .null])
XCTAssertEqual(
error.description,
"Record not found in table place: [id:NULL]")
}
do {
let error = PersistenceError.recordNotFound(
databaseTableName: "user",
key: ["uuid": "E621E1F8-C36C-495A-93FC-0C247A3E6E5F".databaseValue])
XCTAssertEqual(
error.description,
"Record not found in table user: [uuid:\"E621E1F8-C36C-495A-93FC-0C247A3E6E5F\"]")
}
}
}

View File

@ -115,8 +115,10 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "minimalRowIDs")
XCTAssertEqual(key, ["id": record.id.databaseValue])
}
}
}
@ -142,8 +144,10 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "minimalRowIDs")
XCTAssertEqual(key, ["id": record.id.databaseValue])
}
}
}

View File

@ -114,8 +114,10 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "minimalSingles")
XCTAssertEqual(key, ["UUID": "theUUID".databaseValue])
}
}
}
@ -143,8 +145,10 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "minimalSingles")
XCTAssertEqual(key, ["UUID": "theUUID".databaseValue])
}
}
}

View File

@ -169,8 +169,10 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "persons")
XCTAssertEqual(key, ["rowid": .null])
}
}
}
@ -182,8 +184,10 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "persons")
XCTAssertEqual(key, ["rowid": record.id.databaseValue])
}
}
}
@ -210,8 +214,10 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "persons")
XCTAssertEqual(key, ["rowid": record.id.databaseValue])
}
}
}

View File

@ -124,8 +124,10 @@ class RecordPrimaryKeyMultipleTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "citizenships")
XCTAssertEqual(key, ["countryName": .null, "personName": .null])
}
}
}
@ -137,8 +139,10 @@ class RecordPrimaryKeyMultipleTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "citizenships")
XCTAssertEqual(key, ["countryName": "France".databaseValue, "personName": "Arthur".databaseValue])
}
}
}
@ -165,8 +169,10 @@ class RecordPrimaryKeyMultipleTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "citizenships")
XCTAssertEqual(key, ["countryName": "France".databaseValue, "personName": "Arthur".databaseValue])
}
}
}

View File

@ -166,8 +166,10 @@ class RecordPrimaryKeyRowIDTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "persons")
XCTAssertEqual(key, ["id": .null])
}
}
}
@ -179,8 +181,10 @@ class RecordPrimaryKeyRowIDTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "persons")
XCTAssertEqual(key, ["id": record.id.databaseValue])
}
}
}
@ -207,8 +211,10 @@ class RecordPrimaryKeyRowIDTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "persons")
XCTAssertEqual(key, ["id": record.id.databaseValue])
}
}
}

View File

@ -117,8 +117,10 @@ class RecordPrimaryKeySingleTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "pets")
XCTAssertEqual(key, ["UUID": .null])
}
}
}
@ -130,8 +132,10 @@ class RecordPrimaryKeySingleTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "pets")
XCTAssertEqual(key, ["UUID": "BobbyUUID".databaseValue])
}
}
}
@ -158,8 +162,10 @@ class RecordPrimaryKeySingleTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "pets")
XCTAssertEqual(key, ["UUID": "BobbyUUID".databaseValue])
}
}
}

View File

@ -121,8 +121,10 @@ class RecordPrimaryKeySingleWithReplaceConflictResolutionTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "emails")
XCTAssertEqual(key, ["email": record.email.databaseValue])
}
}
}
@ -150,8 +152,10 @@ class RecordPrimaryKeySingleWithReplaceConflictResolutionTests: GRDBTestCase {
do {
try record.update(db)
XCTFail("Expected PersistenceError.recordNotFound")
} catch PersistenceError.recordNotFound {
} catch let PersistenceError.recordNotFound(databaseTableName: databaseTableName, key: key) {
// Expected PersistenceError.recordNotFound
XCTAssertEqual(databaseTableName, "emails")
XCTAssertEqual(key, ["email": record.email.databaseValue])
}
}
}