From 8358ae328ae2ac4cd2520c7bb1d6e22ad6e48278 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Nov 2025 11:50:22 -0800 Subject: [PATCH 1/4] Add static `fetch` helpers, `find` Let's support more ergonomics with functions that complement GRDB's functions. This includes static versions of `fetchAll`, etc.: ```diff -try Reminder.all.fetchAll(db) +try Reminder.fetchAll(db) ``` As well as new APIs for finding and unwrapping primary keyed records: ```diff -try Reminder.find(1).fetchOne(db) // Optional +try Reminder.find(db, key: 1) // Reminder ``` This includes a method version, as well: ```swift try Reminder.where(\.isCompleted).find(db, key: 1) ``` --- .../Documentation.docc/SQLiteData.md | 2 + .../Statement+GRDB.swift | 33 +++++++++ .../StructuredQueries+GRDB/Table+GRDB.swift | 69 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift diff --git a/Sources/SQLiteData/Documentation.docc/SQLiteData.md b/Sources/SQLiteData/Documentation.docc/SQLiteData.md index 943a8fa3..543481a1 100644 --- a/Sources/SQLiteData/Documentation.docc/SQLiteData.md +++ b/Sources/SQLiteData/Documentation.docc/SQLiteData.md @@ -300,6 +300,8 @@ with SQLite to take full advantage of GRDB and SQLiteData. - ``StructuredQueriesCore/Statement`` - ``StructuredQueriesCore/SelectStatement`` +- ``StructuredQueriesCore/Table`` +- ``StructuredQueriesCore/PrimaryKeyedTable`` - ``QueryCursor`` ### Observing model data diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift b/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift index a9943ed4..a61eccf5 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift @@ -186,6 +186,39 @@ extension SelectStatement where QueryValue == (), Joins == () { } } +extension SelectStatement where QueryValue == (), From: PrimaryKeyedTable, Joins == () { + /// Returns a single value fetched from the database for a given primary key. + /// + /// - Parameters + /// - db: A database connection. + /// - primaryKey: A primary key identifying a table row. + /// - Returns: A single value decoded from the database. + @inlinable + public func find( + _ db: Database, + key primaryKey: some QueryExpression + ) throws -> From.QueryOutput { + guard let record = try asSelect().find(primaryKey).fetchOne(db) else { + throw NotFound() + } + return record + } + + /// Returns an array of all values fetched for the given primary keys. + /// + /// - Parameters + /// - db: A database connection. + /// - primaryKeys: A sequence of primary keys. + /// - Returns: A single value decoded from the database. + @inlinable + public func find( + _ db: Database, + keys primaryKeys: some Sequence> + ) throws -> [From.QueryOutput] { + try asSelect().find(primaryKeys).fetchAll(db) + } +} + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SelectStatement where QueryValue == () { /// Returns an array of all values fetched from the database. diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift b/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift new file mode 100644 index 00000000..cf59c934 --- /dev/null +++ b/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift @@ -0,0 +1,69 @@ +import StructuredQueriesCore + +extension StructuredQueriesCore.Table { + /// Returns an array of all values fetched from the database. + /// + /// - Parameter db: A database connection. + /// - Returns: An array of all values decoded from the database. + @inlinable + public static func fetchAll(_ db: Database) throws -> [QueryOutput] { + try all.fetchAll(db) + } + + /// Returns a single value fetched from the database. + /// + /// - Parameter db: A database connection. + /// - Returns: A single value decoded from the database. + @inlinable + public static func fetchOne(_ db: Database) throws -> QueryOutput? { + try all.fetchOne(db) + } + + /// Returns the number of rows fetched by the query. + /// + /// - Parameter db: A database connection. + /// - Returns: The number of rows fetched by the query. + @inlinable + public static func fetchCount(_ db: Database) throws -> Int { + try all.fetchCount(db) + } + + /// Returns a cursor to all values fetched from the database. + /// + /// - Parameter db: A database connection. + /// - Returns: A cursor to all values decoded from the database. + @inlinable + public static func fetchCursor(_ db: Database) throws -> QueryCursor { + try all.fetchCursor(db) + } +} + +extension StructuredQueriesCore.PrimaryKeyedTable { + /// Returns a single value fetched from the database for a given primary key. + /// + /// - Parameters + /// - db: A database connection. + /// - primaryKey: A primary key identifying a table row. + /// - Returns: A single value decoded from the database. + @inlinable + public static func find( + _ db: Database, + key primaryKey: some QueryExpression + ) throws -> QueryOutput { + try all.find(db, key: primaryKey) + } + + /// Returns an array of all values fetched for the given primary keys. + /// + /// - Parameters + /// - db: A database connection. + /// - primaryKeys: A sequence of primary keys. + /// - Returns: A single value decoded from the database. + @inlinable + public static func find( + _ db: Database, + keys primaryKeys: some Sequence> + ) throws -> [QueryOutput] { + try all.find(db, keys: primaryKeys) + } +} From 0f60db6e3cd8fb3cc9fdf56744bcf6aa25f369f6 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Nov 2025 11:58:33 -0800 Subject: [PATCH 2/4] wip --- .../SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift | 2 +- Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift b/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift index a61eccf5..76f8c926 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift @@ -211,7 +211,7 @@ extension SelectStatement where QueryValue == (), From: PrimaryKeyedTable, Joins /// - primaryKeys: A sequence of primary keys. /// - Returns: A single value decoded from the database. @inlinable - public func find( + public func fetchAll( _ db: Database, keys primaryKeys: some Sequence> ) throws -> [From.QueryOutput] { diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift b/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift index cf59c934..d0bb8479 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift @@ -60,10 +60,10 @@ extension StructuredQueriesCore.PrimaryKeyedTable { /// - primaryKeys: A sequence of primary keys. /// - Returns: A single value decoded from the database. @inlinable - public static func find( + public static func fetchAll( _ db: Database, keys primaryKeys: some Sequence> ) throws -> [QueryOutput] { - try all.find(db, keys: primaryKeys) + try all.fetchAll(db, keys: primaryKeys) } } From 1e16074df043193ca46b8cdd6561972034c427c0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Nov 2025 12:01:48 -0800 Subject: [PATCH 3/4] remove fetchAll-keys (not pulling its weight) --- .../StructuredQueries+GRDB/Statement+GRDB.swift | 14 -------------- .../StructuredQueries+GRDB/Table+GRDB.swift | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift b/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift index 76f8c926..c2c990c1 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift @@ -203,20 +203,6 @@ extension SelectStatement where QueryValue == (), From: PrimaryKeyedTable, Joins } return record } - - /// Returns an array of all values fetched for the given primary keys. - /// - /// - Parameters - /// - db: A database connection. - /// - primaryKeys: A sequence of primary keys. - /// - Returns: A single value decoded from the database. - @inlinable - public func fetchAll( - _ db: Database, - keys primaryKeys: some Sequence> - ) throws -> [From.QueryOutput] { - try asSelect().find(primaryKeys).fetchAll(db) - } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift b/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift index d0bb8479..8e55d707 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift @@ -52,18 +52,4 @@ extension StructuredQueriesCore.PrimaryKeyedTable { ) throws -> QueryOutput { try all.find(db, key: primaryKey) } - - /// Returns an array of all values fetched for the given primary keys. - /// - /// - Parameters - /// - db: A database connection. - /// - primaryKeys: A sequence of primary keys. - /// - Returns: A single value decoded from the database. - @inlinable - public static func fetchAll( - _ db: Database, - keys primaryKeys: some Sequence> - ) throws -> [QueryOutput] { - try all.fetchAll(db, keys: primaryKeys) - } } From c6ee11566c62c07e679c126ce6976420e329e1e9 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Nov 2025 12:26:51 -0800 Subject: [PATCH 4/4] wip --- Examples/CaseStudies/DynamicQuery.swift | 2 +- Examples/CaseStudies/TransactionDemo.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 20 +------------------ Examples/Reminders/ReminderForm.swift | 2 +- Examples/Reminders/ReminderRow.swift | 4 ++-- Examples/Reminders/RemindersDetail.swift | 4 ++-- .../RemindersDetailsTests.swift | 10 +++++----- Examples/SyncUpTests/SyncUpFormTests.swift | 2 +- 8 files changed, 14 insertions(+), 32 deletions(-) diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index bbc4de0c..cdef9c5c 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -93,7 +93,7 @@ struct DynamicQueryDemo: SwiftUICaseStudy { return try Value( facts: search.fetchAll(db), searchCount: search.fetchCount(db), - totalCount: Fact.all.fetchCount(db) + totalCount: Fact.fetchCount(db) ) } } diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index 0d8bdc28..dc4404eb 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -64,7 +64,7 @@ struct TransactionDemo: SwiftUICaseStudy { func fetch(_ db: Database) throws -> Value { try Value( facts: Fact.order { $0.id.desc() }.fetchAll(db), - count: Fact.all.fetchCount(db) + count: Fact.fetchCount(db) ) } } diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 587d0c8d..a0d54872 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "41e7781e6c506773b6af84af513bcd6d3b1be59d635e6c4c4bd89638368e4629", + "originHash" : "c133bf7d10c8ce1e5d6506c3d2f080eac8b4c8c2827044d53a9b925e903564fd", "pins" : [ { "identity" : "combine-schedulers", @@ -73,24 +73,6 @@ "version" : "1.9.4" } }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-plugin", - "state" : { - "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", - "version" : "1.4.5" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - }, { "identity" : "swift-identified-collections", "kind" : "remoteSourceControl", diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index b7b70ed3..febebdca 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -208,7 +208,7 @@ struct ReminderFormPreview: PreviewProvider { let (remindersList, reminder) = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() return try $0.defaultDatabase.write { db in - let remindersList = try RemindersList.all.fetchOne(db)! + let remindersList = try RemindersList.fetchOne(db)! return ( remindersList, try Reminder.where { $0.remindersListID.eq(remindersList.id) }.fetchOne(db)! diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index efe509ad..c112eaa3 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -162,8 +162,8 @@ struct ReminderRowPreview: PreviewProvider { let _ = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() try $0.defaultDatabase.read { db in - reminder = try Reminder.all.fetchOne(db) - remindersList = try RemindersList.all.fetchOne(db)! + reminder = try Reminder.fetchOne(db) + remindersList = try RemindersList.fetchOne(db)! } } diff --git a/Examples/Reminders/RemindersDetail.swift b/Examples/Reminders/RemindersDetail.swift index aee49d67..b64d8110 100644 --- a/Examples/Reminders/RemindersDetail.swift +++ b/Examples/Reminders/RemindersDetail.swift @@ -367,8 +367,8 @@ struct RemindersDetailPreview: PreviewProvider { $0.defaultDatabase = try Reminders.appDatabase() return try $0.defaultDatabase.read { db in ( - try RemindersList.all.fetchOne(db)!, - try Tag.all.fetchOne(db)! + try RemindersList.fetchOne(db)!, + try Tag.fetchOne(db)! ) } } diff --git a/Examples/RemindersTests/RemindersDetailsTests.swift b/Examples/RemindersTests/RemindersDetailsTests.swift index 5bec4b80..18ea8fb6 100644 --- a/Examples/RemindersTests/RemindersDetailsTests.swift +++ b/Examples/RemindersTests/RemindersDetailsTests.swift @@ -12,7 +12,7 @@ extension BaseTestSuite { @Dependency(\.defaultDatabase) var database @Test func basics() async throws { - let remindersList = try await database.read { try RemindersList.all.fetchOne($0)! } + let remindersList = try await database.read { try RemindersList.fetchOne($0)! } let model = RemindersDetailModel(detailType: .remindersList(remindersList)) try await model.$reminderRows.load() assertInlineSnapshot(of: model.reminderRows, as: .customDump) { @@ -118,7 +118,7 @@ extension BaseTestSuite { } @Test func ordering() async throws { - let remindersList = try await database.read { try RemindersList.all.fetchOne($0)! } + let remindersList = try await database.read { try RemindersList.fetchOne($0)! } let model = RemindersDetailModel(detailType: .remindersList(remindersList)) try await model.$reminderRows.load() @@ -164,7 +164,7 @@ extension BaseTestSuite { } @Test func showCompleted() async throws { - let remindersList = try await database.read { try RemindersList.all.fetchOne($0)! } + let remindersList = try await database.read { try RemindersList.fetchOne($0)! } let model = RemindersDetailModel(detailType: .remindersList(remindersList)) try await model.$reminderRows.load() @@ -211,7 +211,7 @@ extension BaseTestSuite { } @Test func move() async throws { - let remindersList = try await database.read { try RemindersList.all.fetchOne($0)! } + let remindersList = try await database.read { try RemindersList.fetchOne($0)! } let model = RemindersDetailModel(detailType: .remindersList(remindersList)) try await model.$reminderRows.load() @@ -319,7 +319,7 @@ extension BaseTestSuite { } @Test func tagged() async throws { - let tag = try await database.read { try Tag.find("someday").fetchOne($0)! } + let tag = try await database.read { try Tag.find($0, key: "someday") } let model = RemindersDetailModel(detailType: .tags([tag])) try await model.$reminderRows.load() assertInlineSnapshot(of: model.reminderRows.map(\.reminder.title), as: .customDump) { diff --git a/Examples/SyncUpTests/SyncUpFormTests.swift b/Examples/SyncUpTests/SyncUpFormTests.swift index e428c38e..81eb3dc1 100644 --- a/Examples/SyncUpTests/SyncUpFormTests.swift +++ b/Examples/SyncUpTests/SyncUpFormTests.swift @@ -39,7 +39,7 @@ struct SyncUpFormTests { @Test func updateExisting() async throws { let existingSyncUp = try await database.read { db in - try #require(try SyncUp.all.fetchOne(db)) + try #require(try SyncUp.fetchOne(db)) } let draft = SyncUp.Draft(existingSyncUp) let model = SyncUpFormModel(syncUp: draft)