From bb789e3deece914f5538b59374ec8b2fe03f1951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 5 Jun 2024 18:40:42 +0200 Subject: [PATCH] QueryInterfaceRequest.deleteAndFetchIds --- CHANGELOG.md | 4 ++ .../Request/QueryInterfaceRequest.swift | 71 +++++++++++++++++++ Tests/GRDBTests/TableRecordDeleteTests.swift | 24 +++++++ 3 files changed, 99 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c11a065357..4b24b020ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,10 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: --- +## Next Release + +- **New**: Added `QueryInterfaceRequest.deleteAndFetchIds(_:)` which returns the set of deleted ids. + ## 6.27.0 Released April 21, 2024 diff --git a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift index 2371ae81d7..1d0d7cdadd 100644 --- a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift @@ -61,6 +61,7 @@ /// ### Batch Delete /// /// - ``deleteAll(_:)`` +/// - ``deleteAndFetchIds(_:)`` /// - ``deleteAndFetchCursor(_:)`` /// - ``deleteAndFetchAll(_:)`` /// - ``deleteAndFetchSet(_:)`` @@ -625,6 +626,41 @@ extension QueryInterfaceRequest { { try Set(deleteAndFetchCursor(db)) } + + /// Executes a `DELETE RETURNING` statement and returns the set of + /// deleted ids. + /// + /// For example: + /// + /// ```swift + /// // Fetch the ids of deleted players + /// // DELETE FROM player RETURNING id + /// let request = Player.all() + /// let deletedPlayerIds = try request.deleteAndFetchIds(db) + /// ``` + /// + /// - important: Make sure you check the documentation of the `RETURNING` + /// clause, which describes important limitations and caveats: + /// . + /// + /// - parameter db: A database connection. + /// - returns: A set of deleted ids. + /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. + @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) // Identifiable + public func deleteAndFetchIds(_ db: Database) + throws -> Set + where RowDecoder: TableRecord & Identifiable, + RowDecoder.ID: Hashable & DatabaseValueConvertible & StatementColumnConvertible + { + let primaryKey = try db.primaryKey(RowDecoder.databaseTableName) + GRDBPrecondition( + primaryKey.columns.count == 1, + "Fetching id requires a single-column primary key in the table \(databaseTableName)") + + let statement = try deleteAndFetchStatement(db, selection: [Column(primaryKey.columns[0])]) + + return try RowDecoder.ID.fetchSet(statement) + } #else /// Returns a `DELETE RETURNING` prepared statement. /// @@ -741,6 +777,41 @@ extension QueryInterfaceRequest { { try Set(deleteAndFetchCursor(db)) } + + /// Executes a `DELETE RETURNING` statement and returns the set of + /// deleted ids. + /// + /// For example: + /// + /// ```swift + /// // Fetch the ids of deleted players + /// // DELETE FROM player RETURNING id + /// let request = Player.all() + /// let deletedPlayerIds = try request.deleteAndFetchIds(db) + /// ``` + /// + /// - important: Make sure you check the documentation of the `RETURNING` + /// clause, which describes important limitations and caveats: + /// . + /// + /// - parameter db: A database connection. + /// - returns: A set of deleted ids. + /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ + public func deleteAndFetchIds(_ db: Database) + throws -> Set + where RowDecoder: TableRecord & Identifiable, + RowDecoder.ID: Hashable & DatabaseValueConvertible & StatementColumnConvertible + { + let primaryKey = try db.primaryKey(RowDecoder.databaseTableName) + GRDBPrecondition( + primaryKey.columns.count == 1, + "Fetching id requires a single-column primary key in the table \(databaseTableName)") + + let statement = try deleteAndFetchStatement(db, selection: [Column(primaryKey.columns[0])]) + + return try RowDecoder.ID.fetchSet(statement) + } #endif } diff --git a/Tests/GRDBTests/TableRecordDeleteTests.swift b/Tests/GRDBTests/TableRecordDeleteTests.swift index 88e9c81745..e45252a555 100644 --- a/Tests/GRDBTests/TableRecordDeleteTests.swift +++ b/Tests/GRDBTests/TableRecordDeleteTests.swift @@ -364,6 +364,30 @@ class TableRecordDeleteTests: GRDBTestCase { } } + @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) // Identifiable + func testRequestDeleteAndFetchIds() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + guard sqlite3_libversion_number() >= 3035000 else { + throw XCTSkip("RETURNING clause is not available") + } +#else + guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { + throw XCTSkip("RETURNING clause is not available") + } +#endif + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try Person(id: 1, name: "Arthur", email: "arthur@example.com").insert(db) + try Person(id: 2, name: "Barbara", email: "barbara@example.com").insert(db) + try Person(id: 3, name: "Craig", email: "craig@example.com").insert(db) + + let request = Person.filter(Column("id") != 2) + let deletedIds = try request.deleteAndFetchIds(db) + XCTAssertEqual(deletedIds, [1, 3]) + } + } + func testJoinedRequestDeleteAll() throws { try makeDatabaseQueue().inDatabase { db in struct Player: MutablePersistableRecord {