From 6f510928799c4551d8e7c6d117621549e2863044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 10 Feb 2024 19:00:13 +0100 Subject: [PATCH] [BREAKING] insertAndFetch, saveAndFetch, and updateAndFetch no longer return optionals. Those optionals were just an annoyance. Now, nil results are replaced with `RecordError.recordNotFound`. Such an error is only expected with the IGNORE conflict policy. --- .../MutablePersistableRecord+Insert.swift | 66 ++++++------ .../MutablePersistableRecord+Save.swift | 48 +++++---- .../MutablePersistableRecord+Update.swift | 101 +++++++++++------- GRDB/Record/PersistableRecord+Insert.swift | 44 ++++---- GRDB/Record/PersistableRecord+Save.swift | 30 +++--- GRDB/Record/TableRecord.swift | 34 ++++++ README.md | 18 ++-- .../MutablePersistableRecordTests.swift | 26 +++-- Tests/GRDBTests/PersistableRecordTests.swift | 8 +- 9 files changed, 228 insertions(+), 147 deletions(-) diff --git a/GRDB/Record/MutablePersistableRecord+Insert.swift b/GRDB/Record/MutablePersistableRecord+Insert.swift index 4b2b1e1d04..e089cb23c1 100644 --- a/GRDB/Record/MutablePersistableRecord+Insert.swift +++ b/GRDB/Record/MutablePersistableRecord+Insert.swift @@ -91,7 +91,6 @@ extension MutablePersistableRecord { extension MutablePersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -108,22 +107,22 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The inserted record, if any. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The inserted record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.insertAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -161,11 +160,10 @@ extension MutablePersistableRecord { /// var partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -174,19 +172,24 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { - try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + let record = self + return try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) } } @@ -266,7 +269,6 @@ extension MutablePersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -283,23 +285,23 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The inserted record, if any. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The inserted record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.insertAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -337,11 +339,10 @@ extension MutablePersistableRecord { /// var partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -350,20 +351,25 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { - try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + let record = self + return try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) } } diff --git a/GRDB/Record/MutablePersistableRecord+Save.swift b/GRDB/Record/MutablePersistableRecord+Save.swift index a9776cc820..6a238ac2f9 100644 --- a/GRDB/Record/MutablePersistableRecord+Save.swift +++ b/GRDB/Record/MutablePersistableRecord+Save.swift @@ -87,7 +87,6 @@ extension MutablePersistableRecord { extension MutablePersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -107,22 +106,22 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The saved record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The saved record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.saveAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -135,26 +134,31 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { + let record = self success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) }) return success!.saved } @@ -211,7 +215,6 @@ extension MutablePersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -231,23 +234,23 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The saved record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The saved record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.saveAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -260,27 +263,32 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { + let record = self success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) }) return success!.saved } diff --git a/GRDB/Record/MutablePersistableRecord+Update.swift b/GRDB/Record/MutablePersistableRecord+Update.swift index 691d77d8d4..8875ad5750 100644 --- a/GRDB/Record/MutablePersistableRecord+Update.swift +++ b/GRDB/Record/MutablePersistableRecord+Update.swift @@ -218,7 +218,6 @@ extension MutablePersistableRecord { extension MutablePersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `UPDATE RETURNING` statement on all columns, and returns a /// new record built from the updated row. /// @@ -226,23 +225,23 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The updated record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The updated record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { try updateAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `UPDATE RETURNING` statement on all columns, and returns a /// new record built from the updated row. /// @@ -251,25 +250,28 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try updateAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -280,12 +282,13 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter modify: A closure that modifies the record. - /// - returns: An updated record, or nil if the record has no change, or - /// in case of a failed update due to the `IGNORE` conflict policy. + /// - returns: An updated record, or nil if the record has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func updateChangesAndFetch( _ db: Database, @@ -297,7 +300,6 @@ extension MutablePersistableRecord { try updateChangesAndFetch(db, onConflict: conflictResolution, as: Self.self, modify: modify) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -310,12 +312,13 @@ extension MutablePersistableRecord { /// - parameter returnedType: The type of the returned record. /// - parameter modify: A closure that modifies the record. /// - returns: A record of type `returnedType`, or nil if the record has - /// no change, or in case of a failed update due to the `IGNORE` - /// conflict policy. + /// no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func updateChangesAndFetch( _ db: Database, @@ -324,10 +327,16 @@ extension MutablePersistableRecord { modify: (inout Self) throws -> Void) throws -> T? { - try updateChangesAndFetch( + let record = self + return try updateChangesAndFetch( db, onConflict: conflictResolution, selection: T.databaseSelection, - fetch: { try T.fetchOne($0) }, + fetch: { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) + }, modify: modify) } @@ -482,7 +491,6 @@ extension MutablePersistableRecord { fetch: fetch) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -495,7 +503,8 @@ extension MutablePersistableRecord { /// - parameter selection: The returned columns (must not be empty). /// - parameter fetch: A function that executes it ``Statement`` argument. /// - parameter modify: A closure that modifies the record. - /// - returns: The result of the `fetch` function. + /// - returns: The result of the `fetch` function, or nil if the record + /// has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the @@ -506,7 +515,7 @@ extension MutablePersistableRecord { _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?, + fetch: (Statement) throws -> T, modify: (inout Self) throws -> Void) throws -> T? { @@ -519,7 +528,6 @@ extension MutablePersistableRecord { fetch: fetch) } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `UPDATE RETURNING` statement on all columns, and returns a /// new record built from the updated row. /// @@ -527,18 +535,19 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The updated record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The updated record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { try updateAndFetch(db, onConflict: conflictResolution, as: Self.self) @@ -552,26 +561,29 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try updateAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -582,12 +594,13 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter modify: A closure that modifies the record. - /// - returns: An updated record, or nil if the record has no change, or - /// in case of a failed update due to the `IGNORE` conflict policy. + /// - returns: An updated record, or nil if the record has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func updateChangesAndFetch( @@ -600,7 +613,6 @@ extension MutablePersistableRecord { try updateChangesAndFetch(db, onConflict: conflictResolution, as: Self.self, modify: modify) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -613,12 +625,13 @@ extension MutablePersistableRecord { /// - parameter returnedType: The type of the returned record. /// - parameter modify: A closure that modifies the record. /// - returns: A record of type `returnedType`, or nil if the record has - /// no change, or in case of a failed update due to the `IGNORE` - /// conflict policy. + /// no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func updateChangesAndFetch( @@ -628,10 +641,16 @@ extension MutablePersistableRecord { modify: (inout Self) throws -> Void) throws -> T? { - try updateChangesAndFetch( + let record = self + return try updateChangesAndFetch( db, onConflict: conflictResolution, selection: T.databaseSelection, - fetch: { try T.fetchOne($0) }, + fetch: { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) + }, modify: modify) } @@ -789,7 +808,6 @@ extension MutablePersistableRecord { fetch: fetch) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -802,7 +820,8 @@ extension MutablePersistableRecord { /// - parameter selection: The returned columns (must not be empty). /// - parameter fetch: A function that executes it ``Statement`` argument. /// - parameter modify: A closure that modifies the record. - /// - returns: The result of the `fetch` function. + /// - returns: The result of the `fetch` function, or nil if the record + /// has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the @@ -814,7 +833,7 @@ extension MutablePersistableRecord { _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?, + fetch: (Statement) throws -> T, modify: (inout Self) throws -> Void) throws -> T? { @@ -839,7 +858,7 @@ extension MutablePersistableRecord { onConflict conflictResolution: Database.ConflictResolution?, from container: PersistenceContainer, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?) + fetch: (Statement) throws -> T) throws -> T? { let changes = try PersistenceContainer(db, self).changesIterator(from: container) @@ -861,7 +880,7 @@ extension MutablePersistableRecord { onConflict conflictResolution: Database.ConflictResolution?, from container: PersistenceContainer, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?) + fetch: (Statement) throws -> T) throws -> T? { let changes = try PersistenceContainer(db, self).changesIterator(from: container) diff --git a/GRDB/Record/PersistableRecord+Insert.swift b/GRDB/Record/PersistableRecord+Insert.swift index b44f3f4328..6292e6a0e5 100644 --- a/GRDB/Record/PersistableRecord+Insert.swift +++ b/GRDB/Record/PersistableRecord+Insert.swift @@ -56,7 +56,6 @@ extension PersistableRecord { extension PersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -94,11 +93,10 @@ extension PersistableRecord { /// let partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -107,19 +105,23 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } @@ -199,7 +201,6 @@ extension PersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -237,11 +238,10 @@ extension PersistableRecord { /// let partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -250,20 +250,24 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } diff --git a/GRDB/Record/PersistableRecord+Save.swift b/GRDB/Record/PersistableRecord+Save.swift index 9ba55346b9..80a9f3f815 100644 --- a/GRDB/Record/PersistableRecord+Save.swift +++ b/GRDB/Record/PersistableRecord+Save.swift @@ -39,7 +39,6 @@ extension PersistableRecord { extension PersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -52,26 +51,30 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) }) return success!.saved } @@ -128,7 +131,6 @@ extension PersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -141,27 +143,31 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) }) return success!.saved } diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index bf6b1a58e9..db6edc45e0 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -29,6 +29,7 @@ import Foundation /// - ``exists(_:id:)`` /// - ``exists(_:key:)-60hf2`` /// - ``exists(_:key:)-6ha6`` +/// - ``recordNotFound(_:)`` /// /// ### Throwing Record Not Found Errors /// @@ -697,6 +698,15 @@ extension TableRecord { public enum RecordError: Error { /// A record does not exist in the database. /// + /// This error can be thrown from methods that update, such as + /// ``MutablePersistableRecord/update(_:onConflict:)``. In this case, + /// the error means that the database was not changed. + /// + /// It can also be thrown from methods that inserts or update with a + /// `RETURNING` clause, and the `IGNORE` conflict policy. In this case, + /// the error notifies that a conflict has prevented the change from + /// being applied. + /// /// - parameters: /// - databaseTableName: The table of the missing record. /// - key: The key of the missing record (column and values). @@ -740,6 +750,30 @@ extension TableRecord { } } +extension TableRecord where Self: EncodableRecord { + /// Returns an error that tells that the record does not exist in + /// the database. + /// + /// - returns: ``RecordError/recordNotFound(databaseTableName:key:)``, or + /// any error that prevented the `RecordError` from being constructed. + public func recordNotFound(_ db: Database) -> any Error { + do { + let databaseTableName = type(of: self).databaseTableName + let primaryKey = try db.primaryKey(databaseTableName) + + let container = try PersistenceContainer(db, self) + let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map { + ($0, container[caseInsensitive: $0]?.databaseValue ?? .null) + }) + return RecordError.recordNotFound( + databaseTableName: databaseTableName, + key: key) + } catch { + return error + } + } +} + @available(iOS 13, macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns an error for a record that does not exist in the database. diff --git a/README.md b/README.md index f02c02a46e..761c3d6c18 100644 --- a/README.md +++ b/README.md @@ -2330,11 +2330,10 @@ try dbQueue.write { db in let partialPlayer = PartialPlayer(name: "Alice") // INSERT INTO player (name) VALUES ('Alice') RETURNING * - if let player = try partialPlayer.insertAndFetch(db, as: Player.self) { - print(player.id) // The inserted id - print(player.name) // The inserted name - print(player.score) // The default score - } + let player = try partialPlayer.insertAndFetch(db, as: Player.self) + print(player.id) // The inserted id + print(player.name) // The inserted name + print(player.score) // The default score } ``` @@ -3108,15 +3107,16 @@ struct Player : MutablePersistableRecord { try player.insert(db) ``` -> **Note**: If you specify the `ignore` policy for inserts, the [`didInsert` callback](#persistence-callbacks) will be called with some random id in case of failed insert. You can detect failed insertions with `insertAndFetch`: +> **Note**: If you specify the `ignore` policy for inserts, the [`didInsert` callback](#persistence-callbacks) will be called with some random id in case of failed insert. You can detect failed insertions with `insertAndFetch`: > > ```swift > // How to detect failed `INSERT OR IGNORE`: > // INSERT OR IGNORE INTO player ... RETURNING * -> if let insertedPlayer = try player.insertAndFetch(db) { +> do { +> let insertedPlayer = try player.insertAndFetch(db) { > // Succesful insertion -> } else { -> // Ignored failure +> catch RecordError.recordNotFound { +> // Failed insertion due to IGNORE policy > } > ``` > diff --git a/Tests/GRDBTests/MutablePersistableRecordTests.swift b/Tests/GRDBTests/MutablePersistableRecordTests.swift index 18a15c1eeb..7348cbbbb6 100644 --- a/Tests/GRDBTests/MutablePersistableRecordTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordTests.swift @@ -1261,7 +1261,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let player = FullPlayer(id: nil, name: "Arthur", score: 1000) - let insertedPlayer = try XCTUnwrap(player.insertAndFetch(db)) + let insertedPlayer = try player.insertAndFetch(db) XCTAssertEqual(insertedPlayer.id, 1) XCTAssertEqual(insertedPlayer.name, "Arthur") XCTAssertEqual(insertedPlayer.score, 1000) @@ -1284,7 +1284,7 @@ extension MutablePersistableRecordTests { do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.insertAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING * @@ -1423,7 +1423,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let player = FullPlayer(id: nil, name: "Arthur", score: 1000) - let savedPlayer = try XCTUnwrap(player.saveAndFetch(db)) + let savedPlayer = try player.saveAndFetch(db) XCTAssertEqual(savedPlayer.id, 1) XCTAssertEqual(savedPlayer.name, "Arthur") XCTAssertEqual(savedPlayer.score, 1000) @@ -1446,7 +1446,7 @@ extension MutablePersistableRecordTests { do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("UPDATE") }) XCTAssert(sqlQueries.contains(""" @@ -1483,7 +1483,7 @@ extension MutablePersistableRecordTests { var partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) sqlQueries.removeAll() - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING * @@ -1521,7 +1521,7 @@ extension MutablePersistableRecordTests { do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(id: 1, name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("INSERT") }) XCTAssert(sqlQueries.contains(""" @@ -1709,7 +1709,7 @@ extension MutablePersistableRecordTests { player.name = "Barbara" do { - let updatedPlayer = try XCTUnwrap(player.updateAndFetch(db)) + let updatedPlayer = try player.updateAndFetch(db) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Barbara") XCTAssertEqual(updatedPlayer.score, 1000) @@ -1760,7 +1760,7 @@ extension MutablePersistableRecordTests { player.name = "Barbara" do { - let updatedPlayer = try XCTUnwrap(player.updateAndFetch(db, as: PartialPlayer.self)) + let updatedPlayer = try player.updateAndFetch(db, as: PartialPlayer.self) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Barbara") } @@ -2041,11 +2041,15 @@ extension MutablePersistableRecordTests { try player.insert(db) do { - let updatedRow = try player.updateChangesAndFetch( + // Update with no change + let update = try player.updateChangesAndFetch( db, selection: [AllColumns()], - fetch: { statement in try Row.fetchOne(statement) }, + fetch: { statement in + XCTFail("Should not be called") + return "ignored" + }, modify: { $0.name = "Barbara" }) - XCTAssertNil(updatedRow) + XCTAssertNil(update) } do { diff --git a/Tests/GRDBTests/PersistableRecordTests.swift b/Tests/GRDBTests/PersistableRecordTests.swift index d919eeb989..5c9bc6761b 100644 --- a/Tests/GRDBTests/PersistableRecordTests.swift +++ b/Tests/GRDBTests/PersistableRecordTests.swift @@ -1342,7 +1342,7 @@ extension PersistableRecordTests { do { sqlQueries.removeAll() let partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.insertAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING * @@ -1444,7 +1444,7 @@ extension PersistableRecordTests { do { sqlQueries.removeAll() let partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("UPDATE") }) XCTAssert(sqlQueries.contains(""" @@ -1480,7 +1480,7 @@ extension PersistableRecordTests { let partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) sqlQueries.removeAll() - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING * @@ -1517,7 +1517,7 @@ extension PersistableRecordTests { do { sqlQueries.removeAll() let partialPlayer = PartialPlayer(id: 1, name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("INSERT") }) XCTAssert(sqlQueries.contains("""