Skip to content

Commit 4cbf442

Browse files
authored
Merge pull request groue#1299 from groue/dev/find
Record.find() convenience methods
2 parents 08ec50f + 2bde3e1 commit 4cbf442

24 files changed

+658
-152
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
102102

103103
---
104104

105+
## Next Release
106+
107+
- **New**: [#1299](https://github.com/groue/GRDB.swift/pull/1299) by [@groue](https://github.com/groue): Record.find() convenience methods
108+
105109
## 6.4.0
106110

107111
Released November 28, 2022 • [diff](https://github.com/groue/GRDB.swift/compare/v6.3.1...v6.4.0)

GRDB/Documentation.docc/QueryInterface.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ Record types and the query interface build SQL queries for you.
2323
- ``QueryInterfaceRequest``
2424
- ``Table``
2525

26+
### Errors
27+
28+
- ``RecordError``
29+
- ``PersistenceError``
30+
2631
### Supporting Types
2732

2833
- ``DerivableRequest``

GRDB/Record/FetchableRecord+TableRecord.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,32 @@ extension FetchableRecord where Self: TableRecord {
189189
}
190190
return try filter(key: key).fetchOne(db)
191191
}
192+
193+
/// Returns the record identified by a primary key, or throws an error if
194+
/// the record does not exist.
195+
///
196+
/// For example:
197+
///
198+
/// ```swift
199+
/// try dbQueue.read { db in
200+
/// let player = try Player.find(db, key: 123)
201+
/// let country = try Country.find(db, key: "FR")
202+
/// }
203+
/// ```
204+
///
205+
/// - parameters:
206+
/// - db: A database connection.
207+
/// - key: A primary key value.
208+
/// - returns: A record.
209+
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or a
210+
/// ``RecordError/recordNotFound(databaseTableName:key:)`` if the record
211+
/// does not exist in the database.
212+
public static func find(_ db: Database, key: some DatabaseValueConvertible) throws -> Self {
213+
guard let record = try fetchOne(db, key: key) else {
214+
throw recordNotFound(db, key: key)
215+
}
216+
return record
217+
}
192218
}
193219

194220
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6, *)
@@ -278,6 +304,29 @@ extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseVa
278304
public static func fetchOne(_ db: Database, id: ID) throws -> Self? {
279305
try filter(id: id).fetchOne(db)
280306
}
307+
308+
/// Returns the record identified by a primary key, or throws an error if
309+
/// the record does not exist.
310+
///
311+
/// For example:
312+
///
313+
/// ```swift
314+
/// try dbQueue.read { db in
315+
/// let player = try Player.find(db, id: 123)
316+
/// let country = try Country.find(db, id: "FR")
317+
/// }
318+
/// ```
319+
///
320+
/// - parameters:
321+
/// - db: A database connection.
322+
/// - id: A primary key value.
323+
/// - returns: A record.
324+
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or a
325+
/// ``RecordError/recordNotFound(databaseTableName:key:)`` if the record
326+
/// does not exist in the database.
327+
public static func find(_ db: Database, id: ID) throws -> Self {
328+
try find(db, key: id)
329+
}
281330
}
282331

283332
extension FetchableRecord where Self: TableRecord & Hashable {
@@ -431,6 +480,32 @@ extension FetchableRecord where Self: TableRecord {
431480
}
432481
return try filter(key: key).fetchOne(db)
433482
}
483+
484+
/// Returns the record identified by a unique key (the primary key or
485+
/// any key with a unique index on it), or throws an error if the record
486+
/// does not exist.
487+
///
488+
/// For example:
489+
///
490+
/// ```swift
491+
/// try dbQueue.read { db in
492+
/// let player = try Player.find(db, key: ["name": "Arthur"])
493+
/// }
494+
/// ```
495+
///
496+
/// - parameters:
497+
/// - db: A database connection.
498+
/// - key: A key dictionary.
499+
/// - returns: A record.
500+
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or a
501+
/// ``RecordError/recordNotFound(databaseTableName:key:)`` if the record
502+
/// does not exist in the database.
503+
public static func find(_ db: Database, key: [String: (any DatabaseValueConvertible)?]) throws -> Self {
504+
guard let record = try filter(key: key).fetchOne(db) else {
505+
throw recordNotFound(key: key)
506+
}
507+
return record
508+
}
434509
}
435510

436511
extension FetchableRecord where Self: TableRecord & Hashable {

GRDB/Record/FetchableRecord.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ import Foundation
7575
/// - ``fetchAll(_:keys:)-4c8no``
7676
/// - ``fetchSet(_:keys:)-e6uy``
7777
/// - ``fetchOne(_:key:)-3f3hc``
78+
/// - ``find(_:id:)``
79+
/// - ``find(_:key:)-4kry5``
80+
/// - ``find(_:key:)-1dfbe``
7881
///
7982
/// ### Fetching Record by Key
8083
///

GRDB/Record/MutablePersistableRecord+DAO.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,12 +226,12 @@ final class DAO<Record: MutablePersistableRecord> {
226226
return statement
227227
}
228228

229-
/// Throws a PersistenceError.recordNotFound error
230-
func makeRecordNotFoundError() -> Error {
229+
/// Throws a RecordError.recordNotFound error
230+
func recordNotFound() throws -> Never {
231231
let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map {
232232
($0, persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null)
233233
})
234-
return PersistenceError.recordNotFound(
234+
throw RecordError.recordNotFound(
235235
databaseTableName: databaseTableName,
236236
key: key)
237237
}

GRDB/Record/MutablePersistableRecord+Save.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ extension MutablePersistableRecord {
377377
columns: columns,
378378
selection: selection,
379379
fetch: fetch)
380-
} catch PersistenceError.recordNotFound(databaseTableName: type(of: self).databaseTableName, key: key) {
380+
} catch RecordError.recordNotFound(databaseTableName: type(of: self).databaseTableName, key: key) {
381381
// No row was updated: fallback on insert.
382382
}
383383
}

GRDB/Record/MutablePersistableRecord+Update.swift

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ extension MutablePersistableRecord {
3939
/// - parameter columns: The columns to update.
4040
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
4141
/// error thrown by the persistence callbacks defined by the record type,
42-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
42+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
4343
/// primary key does not match any row in the database.
4444
@inlinable // allow specialization so that empty callbacks are removed
4545
public func update<Columns>(
@@ -83,7 +83,7 @@ extension MutablePersistableRecord {
8383
/// - parameter columns: The columns to update.
8484
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
8585
/// error thrown by the persistence callbacks defined by the record type,
86-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
86+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
8787
/// primary key does not match any row in the database.
8888
@inlinable // allow specialization so that empty callbacks are removed
8989
public func update<Columns>(
@@ -115,7 +115,7 @@ extension MutablePersistableRecord {
115115
/// is used.
116116
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
117117
/// error thrown by the persistence callbacks defined by the record type,
118-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
118+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
119119
/// primary key does not match any row in the database.
120120
@inlinable // allow specialization so that empty callbacks are removed
121121
public func update(
@@ -158,7 +158,7 @@ extension MutablePersistableRecord {
158158
/// - returns: Whether the record had changes.
159159
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
160160
/// error thrown by the persistence callbacks defined by the record type,
161-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
161+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
162162
/// primary key does not match any row in the database.
163163
/// - SeeAlso: updateChanges(_:with:)
164164
@discardableResult
@@ -202,7 +202,7 @@ extension MutablePersistableRecord {
202202
/// - returns: Whether the record had changes.
203203
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
204204
/// error thrown by the persistence callbacks defined by the record type,
205-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
205+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
206206
/// primary key does not match any row in the database.
207207
@discardableResult
208208
@inlinable // allow specialization so that empty callbacks are removed
@@ -233,7 +233,7 @@ extension MutablePersistableRecord {
233233
/// conflict policy is `IGNORE`.
234234
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
235235
/// error thrown by the persistence callbacks defined by the record type,
236-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
236+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
237237
/// primary key does not match any row in the database.
238238
@inlinable // allow specialization so that empty callbacks are removed
239239
public func updateAndFetch(
@@ -257,7 +257,7 @@ extension MutablePersistableRecord {
257257
/// the conflict policy is `IGNORE`.
258258
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
259259
/// error thrown by the persistence callbacks defined by the record type,
260-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
260+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
261261
/// primary key does not match any row in the database.
262262
@inlinable // allow specialization so that empty callbacks are removed
263263
public func updateAndFetch<T: FetchableRecord & TableRecord>(
@@ -285,7 +285,7 @@ extension MutablePersistableRecord {
285285
/// in case of a failed update due to the `IGNORE` conflict policy.
286286
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
287287
/// error thrown by the persistence callbacks defined by the record type,
288-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
288+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
289289
/// primary key does not match any row in the database.
290290
@inlinable // allow specialization so that empty callbacks are removed
291291
public mutating func updateChangesAndFetch(
@@ -314,7 +314,7 @@ extension MutablePersistableRecord {
314314
/// conflict policy.
315315
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
316316
/// error thrown by the persistence callbacks defined by the record type,
317-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
317+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
318318
/// primary key does not match any row in the database.
319319
@inlinable // allow specialization so that empty callbacks are removed
320320
public mutating func updateChangesAndFetch<T: FetchableRecord & TableRecord>(
@@ -358,7 +358,7 @@ extension MutablePersistableRecord {
358358
/// - returns: The result of the `fetch` function.
359359
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
360360
/// error thrown by the persistence callbacks defined by the record type,
361-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
361+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
362362
/// primary key does not match any row in the database.
363363
/// - precondition: `selection` is not empty.
364364
@inlinable // allow specialization so that empty callbacks are removed
@@ -419,7 +419,7 @@ extension MutablePersistableRecord {
419419
/// - returns: The result of the `fetch` function.
420420
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
421421
/// error thrown by the persistence callbacks defined by the record type,
422-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
422+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
423423
/// primary key does not match any row in the database.
424424
/// - precondition: `selection` is not empty.
425425
@inlinable // allow specialization so that empty callbacks are removed
@@ -462,7 +462,7 @@ extension MutablePersistableRecord {
462462
/// - returns: The result of the `fetch` function.
463463
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
464464
/// error thrown by the persistence callbacks defined by the record type,
465-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
465+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
466466
/// primary key does not match any row in the database.
467467
/// - precondition: `selection` is not empty.
468468
@inlinable // allow specialization so that empty callbacks are removed
@@ -497,7 +497,7 @@ extension MutablePersistableRecord {
497497
/// - returns: The result of the `fetch` function.
498498
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
499499
/// error thrown by the persistence callbacks defined by the record type,
500-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
500+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
501501
/// primary key does not match any row in the database.
502502
/// - precondition: `selection` is not empty.
503503
@inlinable // allow specialization so that empty callbacks are removed
@@ -529,7 +529,7 @@ extension MutablePersistableRecord {
529529
/// conflict policy is `IGNORE`.
530530
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
531531
/// error thrown by the persistence callbacks defined by the record type,
532-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
532+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
533533
/// primary key does not match any row in the database.
534534
@inlinable // allow specialization so that empty callbacks are removed
535535
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) // SQLite 3.35.0+
@@ -554,7 +554,7 @@ extension MutablePersistableRecord {
554554
/// the conflict policy is `IGNORE`.
555555
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
556556
/// error thrown by the persistence callbacks defined by the record type,
557-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
557+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
558558
/// primary key does not match any row in the database.
559559
@inlinable // allow specialization so that empty callbacks are removed
560560
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) // SQLite 3.35.0+
@@ -583,7 +583,7 @@ extension MutablePersistableRecord {
583583
/// in case of a failed update due to the `IGNORE` conflict policy.
584584
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
585585
/// error thrown by the persistence callbacks defined by the record type,
586-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
586+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
587587
/// primary key does not match any row in the database.
588588
@inlinable // allow specialization so that empty callbacks are removed
589589
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) // SQLite 3.35.0+
@@ -613,7 +613,7 @@ extension MutablePersistableRecord {
613613
/// conflict policy.
614614
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
615615
/// error thrown by the persistence callbacks defined by the record type,
616-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
616+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
617617
/// primary key does not match any row in the database.
618618
@inlinable // allow specialization so that empty callbacks are removed
619619
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) // SQLite 3.35.0+
@@ -658,7 +658,7 @@ extension MutablePersistableRecord {
658658
/// - returns: The result of the `fetch` function.
659659
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
660660
/// error thrown by the persistence callbacks defined by the record type,
661-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
661+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
662662
/// primary key does not match any row in the database.
663663
/// - precondition: `selection` is not empty.
664664
@inlinable // allow specialization so that empty callbacks are removed
@@ -720,7 +720,7 @@ extension MutablePersistableRecord {
720720
/// - returns: The result of the `fetch` function.
721721
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
722722
/// error thrown by the persistence callbacks defined by the record type,
723-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
723+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
724724
/// primary key does not match any row in the database.
725725
/// - precondition: `selection` is not empty.
726726
@inlinable // allow specialization so that empty callbacks are removed
@@ -764,7 +764,7 @@ extension MutablePersistableRecord {
764764
/// - returns: The result of the `fetch` function.
765765
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
766766
/// error thrown by the persistence callbacks defined by the record type,
767-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
767+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
768768
/// primary key does not match any row in the database.
769769
/// - precondition: `selection` is not empty.
770770
@inlinable // allow specialization so that empty callbacks are removed
@@ -800,7 +800,7 @@ extension MutablePersistableRecord {
800800
/// - returns: The result of the `fetch` function.
801801
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
802802
/// error thrown by the persistence callbacks defined by the record type,
803-
/// or ``PersistenceError/recordNotFound(databaseTableName:key:)`` if the
803+
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
804804
/// primary key does not match any row in the database.
805805
/// - precondition: `selection` is not empty.
806806
@inlinable // allow specialization so that empty callbacks are removed
@@ -942,12 +942,12 @@ extension MutablePersistableRecord {
942942
returning: selection)
943943
else {
944944
// Nil primary key
945-
throw dao.makeRecordNotFoundError()
945+
try dao.recordNotFound()
946946
}
947947
let returned = try fetch(statement)
948948
if db.changesCount == 0 {
949949
// No row was updated
950-
throw dao.makeRecordNotFoundError()
950+
try dao.recordNotFound()
951951
}
952952
let updated = PersistenceSuccess(persistenceContainer: dao.persistenceContainer)
953953
return (updated, returned)

GRDB/Record/MutablePersistableRecord.swift

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@
7373
/// - ``update(_:onConflict:columns:)-5hxyx``
7474
/// - ``updateChanges(_:onConflict:from:)``
7575
/// - ``updateChanges(_:onConflict:modify:)``
76-
/// - ``PersistenceError``
7776
///
7877
/// ### Updating a Record and Fetching the Updated Row
7978
///
@@ -343,28 +342,6 @@ extension MutablePersistableRecord {
343342
}
344343
}
345344

346-
/// An error thrown by persistence methods of the
347-
/// ``MutablePersistableRecord`` protocol.
348-
public enum PersistenceError: Error {
349-
/// Thrown by ``MutablePersistableRecord`` updating methods, when no
350-
/// matching row could be found and updated.
351-
///
352-
/// - parameters:
353-
/// - databaseTableName: The table of the unfound record.
354-
/// - key: The key of the unfound record (column and values).
355-
case recordNotFound(databaseTableName: String, key: [String: DatabaseValue])
356-
}
357-
358-
extension PersistenceError: CustomStringConvertible {
359-
public var description: String {
360-
switch self {
361-
case let .recordNotFound(databaseTableName: databaseTableName, key: key):
362-
let row = Row(key) // For nice output
363-
return "Key not found in table \(databaseTableName): \(row.description)"
364-
}
365-
}
366-
}
367-
368345
/// The `MutablePersistableRecord` protocol uses this type in order to handle
369346
/// SQLite conflicts when records are inserted or updated.
370347
///

0 commit comments

Comments
 (0)