From 22967fc7841f2c2cc10aba8e3e03d243a0bf62ac Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 18 Feb 2025 11:06:26 -0800 Subject: [PATCH 001/171] wip --- Examples/Examples.xcodeproj/project.pbxproj | 7 + Examples/Reminders/ReminderForm.swift | 81 +++++-- Examples/Reminders/ReminderRow.swift | 10 +- Examples/Reminders/RemindersListDetail.swift | 107 +++++----- Examples/Reminders/RemindersListForm.swift | 28 ++- Examples/Reminders/RemindersListRow.swift | 8 +- Examples/Reminders/RemindersLists.swift | 39 ++-- Examples/Reminders/Schema.swift | 199 +++++++++--------- Examples/Reminders/SearchReminders.swift | 9 +- Package.swift | 33 ++- .../xcshareddata/swiftpm/Package.resolved | 11 +- Sources/SharingGRDB/FetchKey.swift | 2 +- Sources/SharingGRDB/Internal/Exports.swift | 1 + .../StructuredQueries/StatementKey.swift | 101 +++++++++ .../StructuredQueriesGRDB.swift | 2 + .../StructuredQueriesGRDBCore.swift | 146 +++++++++++++ 16 files changed, 575 insertions(+), 209 deletions(-) create mode 100644 Sources/SharingGRDB/StructuredQueries/StatementKey.swift create mode 100644 Sources/StructuredQueriesGRDB/StructuredQueriesGRDB.swift create mode 100644 Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index ca5e7c62..35b61f4a 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = DC5FA7472D4C63D60082743E /* DependenciesMacros */; }; DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A132D4842BF0071F499 /* CasePaths */; }; DCBE8A162D4842C80071F499 /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A152D4842C80071F499 /* SharingGRDB */; }; + DCDCB67A2D64648C0038EB37 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCDCB6792D64648C0038EB37 /* StructuredQueriesGRDB */; }; DCF267392D48437300B680BE /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = DCF267382D48437300B680BE /* SwiftUINavigation */; }; DCF2684A2D4993BC00B680BE /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCF268492D4993BC00B680BE /* SharingGRDB */; }; /* End PBXBuildFile section */ @@ -113,6 +114,7 @@ buildActionMask = 2147483647; files = ( CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */, + DCDCB67A2D64648C0038EB37 /* StructuredQueriesGRDB in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -229,6 +231,7 @@ name = Reminders; packageProductDependencies = ( CAFDD6492D5E823A00EE099E /* SharingGRDB */, + DCDCB6792D64648C0038EB37 /* StructuredQueriesGRDB */, ); productName = Reminders; productReference = CAF836D82D4735AB0047AEB5 /* Reminders.app */; @@ -805,6 +808,10 @@ isa = XCSwiftPackageProductDependency; productName = SharingGRDB; }; + DCDCB6792D64648C0038EB37 /* StructuredQueriesGRDB */ = { + isa = XCSwiftPackageProductDependency; + productName = StructuredQueriesGRDB; + }; DCF267382D48437300B680BE /* SwiftUINavigation */ = { isa = XCSwiftPackageProductDependency; package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 6808bd89..a3ab6844 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -1,17 +1,17 @@ import Dependencies import GRDB import IssueReporting -import Sharing import SharingGRDB +import StructuredQueriesGRDB import SwiftUI struct ReminderFormView: View { - @SharedReader(.fetchAll(sql: #"SELECT * FROM "remindersLists" ORDER BY "name" ASC"#)) - var remindersLists: [RemindersList] + @SharedReader(.fetchAll(RemindersList.order(by: \.name))) var remindersLists @State var isPresentingTagsPopover = false @State var remindersList: RemindersList - @State var reminder: Reminder + let reminderID: Reminder.ID? + @State var reminder: Reminder.Draft @State var selectedTags: [Tag] = [] @Dependency(\.defaultDatabase) private var database @@ -20,12 +20,19 @@ struct ReminderFormView: View { init?(existingReminder: Reminder? = nil, remindersList: RemindersList) { self.remindersList = remindersList if let existingReminder { - reminder = existingReminder - } else if let listID = remindersList.id { - reminder = Reminder(listID: listID) + reminderID = existingReminder.id + reminder = Reminder.Draft( + date: existingReminder.date, + isCompleted: existingReminder.isCompleted, + isFlagged: existingReminder.isFlagged, + listID: existingReminder.listID, + notes: existingReminder.notes, + priority: existingReminder.priority, + title: existingReminder.title + ) } else { - reportIssue("'list.id' is required to be non-nil.") - return nil + reminderID = nil + reminder = Reminder.Draft(listID: remindersList.id) } } @@ -119,17 +126,18 @@ struct ReminderFormView: View { } } .onChange(of: remindersList) { - reminder.listID = remindersList.id! + reminder.listID = remindersList.id } } } - .task { [reminderID = reminder.id] in + .task { do { selectedTags = try await database.read { db in try Tag.all() - .joining(optional: Tag.hasMany(ReminderTag.self)) - .filter(Column("reminderID").detached == reminderID) - .order(Column("name")) + .order(by: \.name) + .leftJoin(ReminderTag.all()) { $0.id == $1.tagID } + .where { $1.reminderID == reminderID } + .select { tag, _ in tag } .fetchAll(db) } } catch { @@ -161,18 +169,48 @@ struct ReminderFormView: View { private func saveButtonTapped() { withErrorReporting { try database.write { db in - try reminder.save(db) - try ReminderTag.filter(Column("reminderID") == reminder.id!).deleteAll(db) - for tag in selectedTags { - _ = try ReminderTag(reminderID: reminder.id!, tagID: tag.id!).saved(db) + // try reminder.save(db) + let updatedReminderID: Reminder.ID + /* + let updatedReminderID = Reminder.upsert(id: reminderID, reminder).returning(\.id) + + // If Draft had `id?`: + let updatedReminderID = Reminder.upsert(reminder).returning(\.id) + */ + if let reminderID { + updatedReminderID = try Reminder + .where { $0.id == reminderID } + .update { + // TODO: + // $0.date = reminder.date + $0.isCompleted = reminder.isCompleted + $0.isFlagged = reminder.isFlagged + $0.listID = reminder.listID + $0.notes = reminder.notes + $0.priority = reminder.priority + $0.title = reminder.title + } + .returning(\.id) + .fetchOne(db)! + // TODO: This should be on this branch on 'main' + try db.execute(ReminderTag.where { $0.reminderID == reminderID }.delete()) + } else { + updatedReminderID = try Reminder.insert(reminder).returning(\.id).fetchOne(db)! } + try db.execute( + ReminderTag.insert( + selectedTags.map { tag in + ReminderTag(reminderID: updatedReminderID, tagID: tag.id) + } + ) + ) } } dismiss() } } -extension Reminder { +extension Reminder.Draft { fileprivate var isDateSet: Bool { get { date != nil } set { date = newValue ? Date() : nil } @@ -186,8 +224,7 @@ extension Optional { } struct TagsPopover: View { - @SharedReader(.fetchAll(sql: #"SELECT * FROM "tags" ORDER BY "name" ASC"#)) - var availableTags: [Tag] + @SharedReader(.fetchAll(Tag.order(by: \.name))) var availableTags @Binding var selectedTags: [Tag] @@ -231,6 +268,8 @@ struct TagsPopover: View { let remindersList = try RemindersList.fetchOne(db)! return ( remindersList, + // TODO: Preview bug, use preview provider +// try Reminder.where { $0.listID == remindersList.id }.fetchOne(db)! try Reminder.filter(Column("listID") == remindersList.id).fetchOne(db)! ) } diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 8a1de4c2..bab3379d 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -1,4 +1,6 @@ import Dependencies +// TODO: Comment this out and the error messages are bad +import StructuredQueriesGRDB import SwiftUI struct ReminderRow: View { @@ -86,9 +88,11 @@ struct ReminderRow: View { private func completeButtonTapped() { withErrorReporting { try database.write { db in - var reminder = reminder - reminder.isCompleted.toggle() - _ = try reminder.saved(db) + try db.execute( + Reminder + .where { $0.id == reminder.id } + .update { $0.isCompleted.toggle() } + ) } } } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 6a72a248..99dc835d 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -1,7 +1,14 @@ import Sharing import SharingGRDB +import StructuredQueriesGRDB import SwiftUI +extension OrderingBuilder { + public static func buildBlock(_ component: [OrderingTerm]...) -> [OrderingTerm] { + component.flatMap { $0 } + } +} + struct RemindersListDetailView: View { @State.SharedReader private var remindersState: [Reminders.Record] @Shared private var ordering: Ordering @@ -23,35 +30,25 @@ struct RemindersListDetailView: View { case .title: Image(systemName: "textformat.characters") } } - var queryString: String { - switch self { - case .dueDate: #""date""# - case .priority: #""priority" DESC, "isFlagged" DESC"# - case .title: #""title""# - } - } } - init?(remindersList: RemindersList) { + init(remindersList: RemindersList) { self.remindersList = remindersList _remindersState = State.SharedReader(value: []) - if let listID = remindersList.id { - _ordering = Shared(wrappedValue: .dueDate, .appStorage("ordering_list_\(listID)")) - _showCompleted = Shared(wrappedValue: false, .appStorage("show_completed_list_\(listID)")) - $remindersState = SharedReader( - .fetch( - Reminders( - listID: listID, - ordering: ordering, - showCompleted: showCompleted - ), - animation: .default - ) + _ordering = Shared(wrappedValue: .dueDate, .appStorage("ordering_list_\(remindersList.id)")) + _showCompleted = Shared( + wrappedValue: false, .appStorage("show_completed_list_\(remindersList.id)") + ) + $remindersState = SharedReader( + .fetch( + Reminders( + listID: remindersList.id, + ordering: ordering, + showCompleted: showCompleted + ), + animation: .default ) - } else { - reportIssue("'list.id' required to be non-nil.") - return nil - } + ) } var body: some View { @@ -123,12 +120,9 @@ struct RemindersListDetailView: View { } private func updateQuery() async throws { - guard let listID = remindersList.id - else { return } - try await $remindersState.load( .fetch( - Reminders(listID: listID, ordering: ordering, showCompleted: showCompleted), + Reminders(listID: remindersList.id, ordering: ordering, showCompleted: showCompleted), animation: .default ) ) @@ -139,27 +133,40 @@ struct RemindersListDetailView: View { let ordering: Ordering let showCompleted: Bool func fetch(_ db: Database) throws -> [Record] { - try Record - .fetchAll( - db, - sql: """ - SELECT - "reminders".*, - group_concat("tags"."name", ',') AS "commaSeparatedTags", - NOT "isCompleted" AND coalesce("reminders"."date", date('now')) < date('now') as "isPastDue" - FROM "reminders" - LEFT JOIN "remindersTags" ON "reminders"."id" = "remindersTags"."reminderID" - LEFT JOIN "tags" ON "remindersTags"."tagID" = "tags"."id" - WHERE - "reminders"."listID" = ? - \(showCompleted ? "" : #"AND NOT "isCompleted""#) - GROUP BY "reminders"."id" - ORDER BY - "reminders"."isCompleted" ASC, - \(ordering.queryString) - """, - arguments: [listID] - ) + return try Reminder + .where { $0.listID == listID } + // TODO: Should `where` return `any Expression` as `@_disfavoredOverload`? + // .where { showCompleted ? true : !$0.isCompleted } + .where { showCompleted || !$0.isCompleted } + .group(by: \.id) + .order { + // TODO: Do we want to support this `buildBlock` + $0.isCompleted + + switch ordering { + case .dueDate: + $0.date + case .priority: + ($0.priority.descending(), $0.isFlagged.descending()) + case .title: + $0.title + } + } + .leftJoin(ReminderTag.all()) { $0.id == $1.reminderID } + // TODO: Overload to fix + .leftJoin(Tag.all()) { $0.1.tagID == $1.id } + .select { + ( + $0.0, + $0.0.isCompleted + && .raw("coalesce(\(bind: $0.0.date), date('now')) < date('now')", as: Bool.self), + $1.name.groupConcat(separator: ",") + // TODO: Ambiguous '??' + // !$0.0.isCompleted /* && ($0.0.date ?? .raw("date('now')")) < .raw("date('now')") */ + ) + } + .fetchAll(db) + .map(Record.init) } struct Record: Decodable, FetchableRecord { var reminder: Reminder @@ -176,7 +183,7 @@ struct RemindersListDetailView: View { let remindersList = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase(inMemory: true) return try $0.defaultDatabase.read { db in - try RemindersList.fetchOne(db)! + try RemindersList.fetchOne(db)! as RemindersList } } NavigationStack { diff --git a/Examples/Reminders/RemindersListForm.swift b/Examples/Reminders/RemindersListForm.swift index ad521dca..aaef311d 100644 --- a/Examples/Reminders/RemindersListForm.swift +++ b/Examples/Reminders/RemindersListForm.swift @@ -6,14 +6,17 @@ import SwiftUI struct RemindersListForm: View { @Dependency(\.defaultDatabase) private var database - @State var remindersList: RemindersList + let remindersListID: RemindersList.ID? + @State var remindersList: RemindersList.Draft @Environment(\.dismiss) var dismiss init(existingList: RemindersList? = nil) { if let existingList { - remindersList = existingList + remindersListID = existingList.id + remindersList = RemindersList.Draft(color: existingList.color, name: existingList.name) } else { - remindersList = RemindersList() + remindersListID = nil + remindersList = RemindersList.Draft() } } @@ -28,7 +31,21 @@ struct RemindersListForm: View { withErrorReporting { do { try database.write { db in - _ = try remindersList.saved(db) + if let remindersListID { + try db.execute( + RemindersList.update( + RemindersList( + id: remindersListID, + color: remindersList.color, + name: remindersList.name + ) + ) + ) + } else { + try db.execute( + RemindersList.insert(remindersList) + ) + } } } } @@ -57,7 +74,8 @@ extension Int { set { guard let components = newValue.components else { return } - self = (Int(components[0] * 255) << 16) + self = + (Int(components[0] * 255) << 16) | (Int(components[1] * 255) << 8) | Int(components[2] * 255) } diff --git a/Examples/Reminders/RemindersListRow.swift b/Examples/Reminders/RemindersListRow.swift index 6f5c0e35..452341b2 100644 --- a/Examples/Reminders/RemindersListRow.swift +++ b/Examples/Reminders/RemindersListRow.swift @@ -1,4 +1,5 @@ import SharingGRDB +import StructuredQueriesGRDB import SwiftUI struct RemindersListRow: View { @@ -22,7 +23,11 @@ struct RemindersListRow: View { Button { withErrorReporting { _ = try database.write { db in - try remindersList.delete(db) + try db.execute( + RemindersList + .where { $0.id == remindersList.id } + .delete() + ) } } } label: { @@ -51,6 +56,7 @@ struct RemindersListRow: View { RemindersListRow( reminderCount: 10, remindersList: RemindersList( + id: 1, name: "Personal" ) ) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 4267a3b4..836f9751 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -2,6 +2,7 @@ import Dependencies import GRDB import Sharing import SharingGRDB +import StructuredQueries import SwiftUI struct RemindersListsView: View { @@ -104,18 +105,16 @@ struct RemindersListsView: View { .searchable(text: $searchText) } - private struct RemindersLists: FetchKeyRequest { + struct RemindersLists: FetchKeyRequest { func fetch(_ db: Database) throws -> [Record] { - try Record.fetchAll( - db, - RemindersList.annotated( - with: RemindersList - .hasMany(Reminder.self) - .filter(!Column("isCompleted")) - .count - ) - ) + try RemindersList + .group(by: \.id) + .join(Reminder.where { !$0.isCompleted }) { $0.id == $1.listID } + .select { ($0, $1.id.count()) } + .fetchAll(db) + .map { Record(reminderCount: $1, remindersList: $0) } } + // TODO: Fix @Selection to support queryfragment changes struct Record: Decodable, FetchableRecord { var reminderCount: Int var remindersList: RemindersList @@ -123,19 +122,15 @@ struct RemindersListsView: View { } private struct Stats: FetchKeyRequest { func fetch(_ db: Database) throws -> Value { - let todayCount = try Int.fetchOne(db, sql: """ - SELECT count(*) - FROM "reminders" - WHERE date("date") = date('now') - """) ?? 0 + let todayCount = try Reminder.count() + .where { .raw("date(\(bind: $0.date)) = date('now')") } + .fetchOne(db) ?? 0 let allCount = try Reminder.fetchCount(db) - let scheduledCount = try Int.fetchOne(db, sql: """ - SELECT count(*) - FROM "reminders" - WHERE date("date") > date('now') - """) ?? 0 - let flaggedCount = try Reminder.filter(Column("isFlagged")).fetchCount(db) - let completedCount = try Reminder.filter(Column("isCompleted")).fetchCount(db) + let scheduledCount = try Reminder.count() + .where { .raw("date(\(bind: $0.date)) > date('now')") } + .fetchOne(db) ?? 0 + let flaggedCount = try Reminder.where(\.isFlagged).count().fetchOne(db) ?? 0 + let completedCount = try Reminder.where(\.isCompleted).count().fetchOne(db) ?? 0 return Value( allCount: allCount, completedCount: completedCount, diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index b2ff5958..ea79d85b 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -2,23 +2,22 @@ import Foundation import GRDB import IssueReporting import SharingGRDB +import StructuredQueriesGRDB +@Table("remindersLists") struct RemindersList: Codable, FetchableRecord, Hashable, Identifiable, MutablePersistableRecord { static let databaseTableName = "remindersLists" - - var id: Int64? + var id: Int64 var color = 0x4a99ef var name = "" - - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } +@Table("reminders") struct Reminder: Codable, Equatable, FetchableRecord, Identifiable, MutablePersistableRecord { static let databaseTableName = "reminders" - var id: Int64? + var id: Int64 + @Column(as: .iso8601) var date: Date? var isCompleted = false var isFlagged = false @@ -32,10 +31,11 @@ struct Reminder: Codable, Equatable, FetchableRecord, Identifiable, MutablePersi } } +@Table("tags") struct Tag: Codable, FetchableRecord, MutablePersistableRecord { static let databaseTableName = "tags" - var id: Int64? + var id: Int64 var name = "" mutating func didInsert(_ inserted: InsertionSuccess) { @@ -43,11 +43,13 @@ struct Tag: Codable, FetchableRecord, MutablePersistableRecord { } } +@Table("remindersTags") struct ReminderTag: Codable, FetchableRecord, MutablePersistableRecord { static let databaseTableName = "remindersTags" - var reminderID: Int64? - var tagID: Int64? + // TODO: Both of these should be non-optional even on 'main' + var reminderID: Int64 + var tagID: Int64 } func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { @@ -73,7 +75,7 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Add reminders lists table") { db in - try db.create(table: RemindersList.databaseTableName) { table in + try db.create(table: RemindersList.name) { table in table.autoIncrementedPrimaryKey("id") table.column("color", .integer).defaults(to: 0x4a99ef).notNull() table.column("name", .text).notNull() @@ -86,7 +88,7 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { table.column("isCompleted", .boolean).defaults(to: false).notNull() table.column("isFlagged", .boolean).defaults(to: false).notNull() table.column("listID", .integer) - .references(RemindersList.databaseTableName, column: "id", onDelete: .cascade) + .references(RemindersList.name, column: "id", onDelete: .cascade) .notNull() table.column("notes", .text).notNull() table.column("priority", .integer) @@ -124,97 +126,102 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { } func createDebugRemindersLists() throws { - _ = try RemindersList(color: 0x4a99ef, name: "Personal").inserted(self) - _ = try RemindersList(color: 0xed8935, name: "Family").inserted(self) - _ = try RemindersList(color: 0xb25dd3, name: "Business").inserted(self) + try execute( + RemindersList.insert { + ($0.color, $0.name) + } values: { + (color: 0x4a99ef, name: "Personal") + (color: 0xed8935, name: "Family") + (color: 0xb25dd3, name: "Business") + } + ) } func createDebugReminders() throws { - _ = try Reminder( - date: Date(), - listID: 1, - notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", - title: "Groceries" - ) - .inserted(self) - _ = try Reminder( - date: Date().addingTimeInterval(-60 * 60 * 24 * 2), - isFlagged: true, - listID: 1, - title: "Haircut" - ) - .inserted(self) - _ = try Reminder( - date: Date(), - listID: 1, - notes: "Ask about diet", - priority: 3, - title: "Doctor appointment" - ) - .inserted(self) - _ = try Reminder( - date: Date().addingTimeInterval(-60 * 60 * 24 * 190), - isCompleted: true, - listID: 1, - title: "Take a walk" - ) - .inserted(self) - _ = try Reminder( - date: Date(), - listID: 1, - title: "Buy concert tickets" - ) - .inserted(self) - _ = try Reminder( - date: Date().addingTimeInterval(60 * 60 * 24 * 2), - isFlagged: true, - listID: 2, - priority: 3, - title: "Pick up kids from school" - ) - .inserted(self) - _ = try Reminder( - date: Date().addingTimeInterval(-60 * 60 * 24 * 2), - isCompleted: true, - listID: 2, - priority: 1, - title: "Get laundry" - ) - .inserted(self) - _ = try Reminder( - date: Date().addingTimeInterval(60 * 60 * 24 * 4), - isCompleted: false, - listID: 2, - priority: 3, - title: "Take out trash" - ) - .inserted(self) - _ = try Reminder( - date: Date().addingTimeInterval(60 * 60 * 24 * 2), - listID: 3, - notes: """ - Status of tax return - Expenses for next year - Changing payroll company - """, - title: "Call accountant" - ) - .inserted(self) - _ = try Reminder( - date: Date().addingTimeInterval(-60 * 60 * 24 * 2), - isCompleted: true, - listID: 3, - priority: 2, - title: "Send weekly emails" + // TODO: Support this? +// _ = try Reminder.Draft( +// date: Date(), +// listID: 1, +// notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", +// title: "Groceries" +// ) +// .inserted(self) + try execute( + Reminder.insert([ + Reminder.Draft( + date: Date(), + listID: 1, + notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", + title: "Groceries" + ), + Reminder.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 2), + isFlagged: true, + listID: 1, + title: "Haircut" + ), + Reminder.Draft( + date: Date(), + listID: 1, + notes: "Ask about diet", + priority: 3, + title: "Doctor appointment" + ), + Reminder.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 190), + isCompleted: true, + listID: 1, + title: "Take a walk" + ), + Reminder.Draft( + date: Date(), + listID: 1, + title: "Buy concert tickets" + ), + Reminder.Draft( + date: Date().addingTimeInterval(60 * 60 * 24 * 2), + isFlagged: true, + listID: 2, + priority: 3, + title: "Pick up kids from school" + ), + Reminder.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 2), + isCompleted: true, + listID: 2, + priority: 1, + title: "Get laundry" + ), + Reminder.Draft( + date: Date().addingTimeInterval(60 * 60 * 24 * 4), + isCompleted: false, + listID: 2, + priority: 3, + title: "Take out trash" + ), + Reminder.Draft( + date: Date().addingTimeInterval(60 * 60 * 24 * 2), + listID: 3, + notes: """ + Status of tax return + Expenses for next year + Changing payroll company + """, + title: "Call accountant" + ), + Reminder.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 2), + isCompleted: true, + listID: 3, + priority: 2, + title: "Send weekly emails" + ), + ]) ) - .inserted(self) } func createDebugTags() throws { - _ = try Tag(name: "car").inserted(self) - _ = try Tag(name: "kids").inserted(self) - _ = try Tag(name: "someday").inserted(self) - _ = try Tag(name: "optional").inserted(self) + try execute(Tag.insert(\.name) { "car"; "kids"; "someday"; "optional" }) _ = try ReminderTag(reminderID: 1, tagID: 3).inserted(self) _ = try ReminderTag(reminderID: 1, tagID: 4).inserted(self) _ = try ReminderTag(reminderID: 2, tagID: 3).inserted(self) diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 281f2b44..4a555214 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -138,10 +138,9 @@ struct SearchRemindersView: View { // NB: We are loading lists as a separate query because we are not sure how to join // "remindersLists" into the above query and decode it into 'State'. Ideally this // could all be done with a single query. - let remindersLists = try RemindersList.fetchAll( - db, - keys: Set(reminders.map(\.remindersListID)) - ) + let remindersLists = try RemindersList + .where { reminders.map(\.remindersListID).contains($0.id) } + .fetchAll(db) let completedCount = try searchQueryBase(searchText: searchText) .filter(Column("isCompleted")) @@ -162,7 +161,7 @@ struct SearchRemindersView: View { struct Value { var completedCount = 0 var reminders: [Reminder] = [] - struct Reminder: Decodable, FetchableRecord { + struct Reminder { var isPastDue: Bool let reminder: Reminders.Reminder let remindersList: RemindersList diff --git a/Package.swift b/Package.swift index 184bc6c2..63cf0c88 100644 --- a/Package.swift +++ b/Package.swift @@ -5,26 +5,36 @@ import PackageDescription let package = Package( name: "sharing-grdb", platforms: [ - .iOS(.v13), - .macOS(.v10_15), - .tvOS(.v13), - .watchOS(.v7), + .iOS(.v16), + .macOS(.v13), + .tvOS(.v16), + .watchOS(.v10), ], products: [ .library( name: "SharingGRDB", targets: ["SharingGRDB"] ), + .library( + name: "StructuredQueriesGRDB", + targets: ["StructuredQueriesGRDB"] + ), + .library( + name: "StructuredQueriesGRDBCore", + targets: ["StructuredQueriesGRDBCore"] + ), ], dependencies: [ .package(url: "https://github.com/groue/GRDB.swift", from: "7.1.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), + .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), ], targets: [ .target( name: "SharingGRDB", dependencies: [ + "StructuredQueriesGRDBCore", .product(name: "GRDB", package: "GRDB.swift"), .product(name: "Sharing", package: "swift-sharing"), ] @@ -36,6 +46,21 @@ let package = Package( .product(name: "DependenciesTestSupport", package: "swift-dependencies"), ] ), + + .target( + name: "StructuredQueriesGRDBCore", + dependencies: [ + .product(name: "GRDB", package: "GRDB.swift"), + .product(name: "StructuredQueriesCore", package: "swift-structured-queries"), + ] + ), + .target( + name: "StructuredQueriesGRDB", + dependencies: [ + "StructuredQueriesGRDBCore", + .product(name: "StructuredQueries", package: "swift-structured-queries"), + ] + ), ], swiftLanguageModes: [.v6] ) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2d264ecd..cf0a4b83 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ba45285fada46a9dabe04dc9ec681721072dab56303b4b2fc2f8b722e7c72992", + "originHash" : "069a55c89e06e2b296b3a54847c62c4c87d976f6a89ded8f3f65b69858c833e6", "pins" : [ { "identity" : "combine-schedulers", @@ -127,6 +127,15 @@ "version" : "2.3.0" } }, + { + "identity" : "swift-structured-queries", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-structured-queries", + "state" : { + "branch" : "main", + "revision" : "3dbff6900efb8a453353931035ead0e8e78f2f65" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", diff --git a/Sources/SharingGRDB/FetchKey.swift b/Sources/SharingGRDB/FetchKey.swift index f08665f6..1c3cc2d7 100644 --- a/Sources/SharingGRDB/FetchKey.swift +++ b/Sources/SharingGRDB/FetchKey.swift @@ -298,4 +298,4 @@ private struct FetchOne: FetchKeyRequest { } } -private struct NotFound: Error {} +struct NotFound: Error {} diff --git a/Sources/SharingGRDB/Internal/Exports.swift b/Sources/SharingGRDB/Internal/Exports.swift index 96421c6b..8768304b 100644 --- a/Sources/SharingGRDB/Internal/Exports.swift +++ b/Sources/SharingGRDB/Internal/Exports.swift @@ -1,3 +1,4 @@ @_exported import Dependencies @_exported import GRDB @_exported import Sharing +@_exported import StructuredQueriesGRDBCore diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift new file mode 100644 index 00000000..139919b4 --- /dev/null +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -0,0 +1,101 @@ +import Dependencies +import Dispatch +import GRDB +import Sharing +import StructuredQueriesCore +import StructuredQueriesGRDBCore + +#if canImport(SwiftUI) + import SwiftUI +#endif + +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) +extension SharedReaderKey { + public static func fetchAll( + _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler = .async(onQueue: .main) + ) -> Self + where Self == FetchKey<[(repeat each Value)]>.Default { + fetch(FetchAllStatementRequest(statement: statement), database: database, scheduler: scheduler) + } + + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler = .async(onQueue: .main) + ) -> Self + where Self == FetchKey<(repeat each Value)> { + fetch(FetchOneStatementRequest(statement: statement), database: database, scheduler: scheduler) + } + + #if canImport(SwiftUI) + public static func fetchAll( + _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, + database: (any DatabaseReader)? = nil, + animation: Animation + ) -> Self + where Self == FetchKey<[(repeat each Value)]>.Default { + fetch( + FetchAllStatementRequest(statement: statement), database: database, animation: animation + ) + } + + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, + database: (any DatabaseReader)? = nil, + animation: Animation + ) -> Self + where Self == FetchKey<(repeat each Value)> { + fetch( + FetchOneStatementRequest(statement: statement), database: database, animation: animation + ) + } + #endif +} + +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) +private struct FetchAllStatementRequest: FetchKeyRequest { + let statement: any StructuredQueriesCore.Statement<[(repeat each Value)]> + + func fetch(_ db: Database) throws -> [(repeat each Value)] { + func open( + _ statement: any StructuredQueriesCore.Statement<[(repeat each Value)]> + ) throws -> [(repeat each Value)] { + try statement.fetchAll(db) + } + return try open(statement) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(statement) + } +} + +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) +private struct FetchOneStatementRequest: FetchKeyRequest { + let statement: any StructuredQueriesCore.Statement<[(repeat each Value)]> + + func fetch(_ db: Database) throws -> (repeat each Value) { + func open( + _ statement: any StructuredQueriesCore.Statement<[(repeat each Value)]> + ) throws -> (repeat each Value) { + guard let result = try statement.fetchOne(db) + else { throw NotFound() } + return result + } + return try open(statement) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(statement) + } +} diff --git a/Sources/StructuredQueriesGRDB/StructuredQueriesGRDB.swift b/Sources/StructuredQueriesGRDB/StructuredQueriesGRDB.swift new file mode 100644 index 00000000..99a11a9d --- /dev/null +++ b/Sources/StructuredQueriesGRDB/StructuredQueriesGRDB.swift @@ -0,0 +1,2 @@ +@_exported import StructuredQueries +@_exported import StructuredQueriesGRDBCore diff --git a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift new file mode 100644 index 00000000..bb90e066 --- /dev/null +++ b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift @@ -0,0 +1,146 @@ +import GRDB +import Foundation +@_exported import StructuredQueriesCore + +extension StructuredQueriesCore.Table { + @_disfavoredOverload + public static func fetchAll(_ db: Database) throws -> [Self] { + try all().fetchAll(db) + } + + @_disfavoredOverload + public static func fetchOne(_ db: Database) throws -> Self? { + try limit(1).fetchOne(db) + } +} + +extension Database { + public func execute(_ statement: some StructuredQueriesCore.Statement) throws { + let query = statement.queryFragment + guard !query.isEmpty else { return } + try execute(sql: query.string, arguments: query.arguments) + } +} + +extension StructuredQueriesCore.Statement { + public func fetchAll( + _ db: Database + ) throws -> [(repeat each Value)] + where QueryOutput == [(repeat each Value)] { + let query = queryFragment + guard !query.isEmpty else { return [] } + let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) + var results: [(repeat each Value)] = [] + let decoder = GRDBQueryDecoder() + while let row = try cursor.next() { + try decoder.withRow(row) { + try results.append((repeat (each Value)(decoder: decoder))) + } + } + return results + } + + public func fetchOne( + _ db: Database + ) throws -> (repeat each Value)? + where QueryOutput == [(repeat each Value)] { + let query = queryFragment + guard !query.isEmpty else { return nil } + let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) + guard let row = try cursor.next() else { return nil } + let decoder = GRDBQueryDecoder() + return try decoder.withRow(row) { + try (repeat (each Value)(decoder: decoder)) + } + } +} + +fileprivate final class GRDBQueryDecoder: QueryDecoder { + private var statement: OpaquePointer? + private var currentIndex: Int = 0 + private var currentRow: Row! + + public init() {} + + func withRow(_ row: Row, body: () throws -> R) rethrows -> R { + currentRow = row + defer { + currentIndex = 0 + currentRow = nil + } + return try body() + } + + func decodeNil() throws -> Bool { + guard currentIndex < currentRow.count else { throw DecodingError() } + let isNil = currentRow.hasNull(atIndex: currentIndex) + if isNil { currentIndex += 1 } + return isNil + } + + func decode(_ type: Double.Type) throws -> Double { + defer { currentIndex += 1 } + guard + currentIndex < currentRow.count, + let value = currentRow[currentIndex] as? Double + else { throw DecodingError() } + return value + } + + func decode(_ type: Int64.Type) throws -> Int64 { + defer { currentIndex += 1 } + guard + currentIndex < currentRow.count, + let value = currentRow[currentIndex] as? Int64 + else { throw DecodingError() } + return value + } + + func decode(_ type: String.Type) throws -> String { + defer { currentIndex += 1 } + guard + currentIndex < currentRow.count, + let value = currentRow[currentIndex] as? String + else { throw DecodingError() } + return value + } + + func decode(_ type: [UInt8].Type) throws -> [UInt8] { + defer { currentIndex += 1 } + guard + currentIndex < currentRow.count, + let value = currentRow[currentIndex] as? Data + else { throw DecodingError() } + return [UInt8](value) + } + + // TODO: Better error handling/messaging + private struct DecodingError: Error { + init() { + print("!!!") + } + } +} + +fileprivate extension QueryFragment { + var arguments: StatementArguments { + StatementArguments(bindings.map(\.databaseValue)) + } +} + +fileprivate extension QueryBinding /* : DatabaseValueConvertible */ { + var databaseValue: DatabaseValue { + switch self { + case .blob(let blob): + return Data(blob).databaseValue + case .double(let double): + return double.databaseValue + case .int(let int): + return int.databaseValue + case .null: + return .null + case .text(let text): + return text.databaseValue + } + } +} From 8e7058c1df8de6c84ebcd8ab1c6b1c8a80f3abf4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 18 Feb 2025 11:36:51 -0800 Subject: [PATCH 002/171] Add overloads --- .../StructuredQueries/StatementKey.swift | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 139919b4..11734296 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -20,6 +20,15 @@ extension SharedReaderKey { fetch(FetchAllStatementRequest(statement: statement), database: database, scheduler: scheduler) } + public static func fetchAll( + _ statement: some StructuredQueriesCore.Statement<[Value]>, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler = .async(onQueue: .main) + ) -> Self + where Self == FetchKey<[Value]>.Default { + fetch(FetchAllStatementRequest(statement: statement), database: database, scheduler: scheduler) + } + public static func fetchOne( _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, database: (any DatabaseReader)? = nil, @@ -29,6 +38,15 @@ extension SharedReaderKey { fetch(FetchOneStatementRequest(statement: statement), database: database, scheduler: scheduler) } + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement<[Value]>, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler = .async(onQueue: .main) + ) -> Self + where Self == FetchKey { + fetch(FetchOneStatementRequest(statement: statement), database: database, scheduler: scheduler) + } + #if canImport(SwiftUI) public static func fetchAll( _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, @@ -41,6 +59,17 @@ extension SharedReaderKey { ) } + public static func fetchAll( + _ statement: some StructuredQueriesCore.Statement<[Value]>, + database: (any DatabaseReader)? = nil, + animation: Animation + ) -> Self + where Self == FetchKey<[Value]>.Default { + fetch( + FetchAllStatementRequest(statement: statement), database: database, animation: animation + ) + } + public static func fetchOne( _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, database: (any DatabaseReader)? = nil, @@ -51,6 +80,17 @@ extension SharedReaderKey { FetchOneStatementRequest(statement: statement), database: database, animation: animation ) } + + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement<[Value]>, + database: (any DatabaseReader)? = nil, + animation: Animation + ) -> Self + where Self == FetchKey { + fetch( + FetchOneStatementRequest(statement: statement), database: database, animation: animation + ) + } #endif } From e69f66b709111cfdddb074ef5f74661e06d1e2a7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 18 Feb 2025 14:30:57 -0800 Subject: [PATCH 003/171] wip --- Examples/Reminders/ReminderForm.swift | 15 +- Examples/Reminders/ReminderRow.swift | 9 +- Examples/Reminders/RemindersListForm.swift | 17 +- Examples/Reminders/RemindersListRow.swift | 9 +- Examples/Reminders/Schema.swift | 154 +++++++++--------- .../StructuredQueriesGRDBCore.swift | 53 ++++-- 6 files changed, 133 insertions(+), 124 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index a3ab6844..17ae4a25 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -193,17 +193,16 @@ struct ReminderFormView: View { .returning(\.id) .fetchOne(db)! // TODO: This should be on this branch on 'main' - try db.execute(ReminderTag.where { $0.reminderID == reminderID }.delete()) + try ReminderTag.where { $0.reminderID == reminderID }.delete().execute(db) } else { - updatedReminderID = try Reminder.insert(reminder).returning(\.id).fetchOne(db)! + updatedReminderID = try Reminder.insert(reminder).returning(\.id).fetchOne(db)!.id } - try db.execute( - ReminderTag.insert( - selectedTags.map { tag in - ReminderTag(reminderID: updatedReminderID, tagID: tag.id) - } - ) + try ReminderTag.insert( + selectedTags.map { tag in + ReminderTag(reminderID: updatedReminderID, tagID: tag.id) + } ) + .execute(db) } } dismiss() diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index bab3379d..69d57e62 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -88,11 +88,10 @@ struct ReminderRow: View { private func completeButtonTapped() { withErrorReporting { try database.write { db in - try db.execute( - Reminder - .where { $0.id == reminder.id } - .update { $0.isCompleted.toggle() } - ) + try Reminder + .where { $0.id == reminder.id } + .update { $0.isCompleted.toggle() } + .execute(db) } } } diff --git a/Examples/Reminders/RemindersListForm.swift b/Examples/Reminders/RemindersListForm.swift index aaef311d..371160d1 100644 --- a/Examples/Reminders/RemindersListForm.swift +++ b/Examples/Reminders/RemindersListForm.swift @@ -32,19 +32,16 @@ struct RemindersListForm: View { do { try database.write { db in if let remindersListID { - try db.execute( - RemindersList.update( - RemindersList( - id: remindersListID, - color: remindersList.color, - name: remindersList.name - ) + try RemindersList.update( + RemindersList( + id: remindersListID, + color: remindersList.color, + name: remindersList.name ) ) + .execute(db) } else { - try db.execute( - RemindersList.insert(remindersList) - ) + try RemindersList.insert(remindersList).execute(db) } } } diff --git a/Examples/Reminders/RemindersListRow.swift b/Examples/Reminders/RemindersListRow.swift index 452341b2..54410670 100644 --- a/Examples/Reminders/RemindersListRow.swift +++ b/Examples/Reminders/RemindersListRow.swift @@ -23,11 +23,10 @@ struct RemindersListRow: View { Button { withErrorReporting { _ = try database.write { db in - try db.execute( - RemindersList - .where { $0.id == remindersList.id } - .delete() - ) + try RemindersList + .where { $0.id == remindersList.id } + .delete() + .execute(db) } } } label: { diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index ea79d85b..54658eac 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -126,15 +126,14 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { } func createDebugRemindersLists() throws { - try execute( - RemindersList.insert { - ($0.color, $0.name) - } values: { - (color: 0x4a99ef, name: "Personal") - (color: 0xed8935, name: "Family") - (color: 0xb25dd3, name: "Business") - } - ) + try RemindersList.insert { + ($0.color, $0.name) + } values: { + (color: 0x4a99ef, name: "Personal") + (color: 0xed8935, name: "Family") + (color: 0xb25dd3, name: "Business") + } + .execute(self) } func createDebugReminders() throws { @@ -146,82 +145,81 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { // title: "Groceries" // ) // .inserted(self) - try execute( - Reminder.insert([ - Reminder.Draft( - date: Date(), - listID: 1, - notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", - title: "Groceries" - ), - Reminder.Draft( - date: Date().addingTimeInterval(-60 * 60 * 24 * 2), - isFlagged: true, - listID: 1, - title: "Haircut" - ), - Reminder.Draft( - date: Date(), - listID: 1, - notes: "Ask about diet", - priority: 3, - title: "Doctor appointment" - ), - Reminder.Draft( - date: Date().addingTimeInterval(-60 * 60 * 24 * 190), - isCompleted: true, - listID: 1, - title: "Take a walk" - ), - Reminder.Draft( - date: Date(), - listID: 1, - title: "Buy concert tickets" - ), - Reminder.Draft( - date: Date().addingTimeInterval(60 * 60 * 24 * 2), - isFlagged: true, - listID: 2, - priority: 3, - title: "Pick up kids from school" - ), - Reminder.Draft( - date: Date().addingTimeInterval(-60 * 60 * 24 * 2), - isCompleted: true, - listID: 2, - priority: 1, - title: "Get laundry" - ), - Reminder.Draft( - date: Date().addingTimeInterval(60 * 60 * 24 * 4), - isCompleted: false, - listID: 2, - priority: 3, - title: "Take out trash" - ), - Reminder.Draft( - date: Date().addingTimeInterval(60 * 60 * 24 * 2), - listID: 3, - notes: """ + try Reminder.insert([ + Reminder.Draft( + date: Date(), + listID: 1, + notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", + title: "Groceries" + ), + Reminder.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 2), + isFlagged: true, + listID: 1, + title: "Haircut" + ), + Reminder.Draft( + date: Date(), + listID: 1, + notes: "Ask about diet", + priority: 3, + title: "Doctor appointment" + ), + Reminder.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 190), + isCompleted: true, + listID: 1, + title: "Take a walk" + ), + Reminder.Draft( + date: Date(), + listID: 1, + title: "Buy concert tickets" + ), + Reminder.Draft( + date: Date().addingTimeInterval(60 * 60 * 24 * 2), + isFlagged: true, + listID: 2, + priority: 3, + title: "Pick up kids from school" + ), + Reminder.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 2), + isCompleted: true, + listID: 2, + priority: 1, + title: "Get laundry" + ), + Reminder.Draft( + date: Date().addingTimeInterval(60 * 60 * 24 * 4), + isCompleted: false, + listID: 2, + priority: 3, + title: "Take out trash" + ), + Reminder.Draft( + date: Date().addingTimeInterval(60 * 60 * 24 * 2), + listID: 3, + notes: """ Status of tax return Expenses for next year Changing payroll company """, - title: "Call accountant" - ), - Reminder.Draft( - date: Date().addingTimeInterval(-60 * 60 * 24 * 2), - isCompleted: true, - listID: 3, - priority: 2, - title: "Send weekly emails" - ), - ]) - ) + title: "Call accountant" + ), + Reminder.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 2), + isCompleted: true, + listID: 3, + priority: 2, + title: "Send weekly emails" + ), + ]) + .execute(self) } func createDebugTags() throws { - try execute(Tag.insert(\.name) { "car"; "kids"; "someday"; "optional" }) + try Tag.insert(\.name) { "car"; "kids"; "someday"; "optional" }.execute(self) _ = try ReminderTag(reminderID: 1, tagID: 3).inserted(self) _ = try ReminderTag(reminderID: 1, tagID: 4).inserted(self) _ = try ReminderTag(reminderID: 2, tagID: 3).inserted(self) diff --git a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift index bb90e066..643a7278 100644 --- a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift +++ b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift @@ -2,27 +2,13 @@ import GRDB import Foundation @_exported import StructuredQueriesCore -extension StructuredQueriesCore.Table { - @_disfavoredOverload - public static func fetchAll(_ db: Database) throws -> [Self] { - try all().fetchAll(db) - } - - @_disfavoredOverload - public static func fetchOne(_ db: Database) throws -> Self? { - try limit(1).fetchOne(db) - } -} - -extension Database { - public func execute(_ statement: some StructuredQueriesCore.Statement) throws { - let query = statement.queryFragment +extension StructuredQueriesCore.Statement { + public func execute(_ db: Database) throws { + let query = queryFragment guard !query.isEmpty else { return } - try execute(sql: query.string, arguments: query.arguments) + try db.execute(sql: query.string, arguments: query.arguments) } -} -extension StructuredQueriesCore.Statement { public func fetchAll( _ db: Database ) throws -> [(repeat each Value)] @@ -40,6 +26,23 @@ extension StructuredQueriesCore.Statement { return results } + public func fetchAll( + _ db: Database + ) throws -> [Value] + where QueryOutput == [Value] { + let query = queryFragment + guard !query.isEmpty else { return [] } + let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) + var results: [Value] = [] + let decoder = GRDBQueryDecoder() + while let row = try cursor.next() { + try decoder.withRow(row) { + try results.append(Value(decoder: decoder)) + } + } + return results + } + public func fetchOne( _ db: Database ) throws -> (repeat each Value)? @@ -53,6 +56,20 @@ extension StructuredQueriesCore.Statement { try (repeat (each Value)(decoder: decoder)) } } + + public func fetchOne( + _ db: Database + ) throws -> Value? + where QueryOutput == [Value] { + let query = queryFragment + guard !query.isEmpty else { return nil } + let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) + guard let row = try cursor.next() else { return nil } + let decoder = GRDBQueryDecoder() + return try decoder.withRow(row) { + try Value(decoder: decoder) + } + } } fileprivate final class GRDBQueryDecoder: QueryDecoder { From 370ceec60c47b85eb7faaff72f7538475e3b4d03 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 18 Feb 2025 17:06:52 -0800 Subject: [PATCH 004/171] wip --- Examples/Reminders/File.swift | 24 +++++ Examples/Reminders/ReminderForm.swift | 7 +- Examples/Reminders/RemindersListDetail.swift | 61 +++++++----- Examples/Reminders/Schema.swift | 29 ++++-- Examples/Reminders/SearchReminders.swift | 93 ++++++------------- Package.swift | 3 +- .../xcshareddata/swiftpm/Package.resolved | 11 +-- 7 files changed, 119 insertions(+), 109 deletions(-) create mode 100644 Examples/Reminders/File.swift diff --git a/Examples/Reminders/File.swift b/Examples/Reminders/File.swift new file mode 100644 index 00000000..bf8d0954 --- /dev/null +++ b/Examples/Reminders/File.swift @@ -0,0 +1,24 @@ +import StructuredQueries +import Foundation + +@Table +struct SyncUp: Codable, Hashable { + var id: Int64? + var isDeleted = false + var seconds = 60 * 5 + var title = "" + static let notDeleted = Self.where { !$0.isDeleted } +} + +@Table +struct Attendee: Codable, Hashable { + var id: Int64? + var isDeleted = false + var name = "" + var syncUpID: Int64 + static let notDeleted = Self.where { !$0.isDeleted } + static let withSyncUp = Attendee.notDeleted + .join(SyncUp.notDeleted) { $0.syncUpID == $1.id } +} + + diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 17ae4a25..e4c4f4ce 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -17,7 +17,7 @@ struct ReminderFormView: View { @Dependency(\.defaultDatabase) private var database @Environment(\.dismiss) var dismiss - init?(existingReminder: Reminder? = nil, remindersList: RemindersList) { + init(existingReminder: Reminder? = nil, remindersList: RemindersList) { self.remindersList = remindersList if let existingReminder { reminderID = existingReminder.id @@ -181,8 +181,7 @@ struct ReminderFormView: View { updatedReminderID = try Reminder .where { $0.id == reminderID } .update { - // TODO: - // $0.date = reminder.date + // TODO: $0.date = reminder.date $0.isCompleted = reminder.isCompleted $0.isFlagged = reminder.isFlagged $0.listID = reminder.listID @@ -195,7 +194,7 @@ struct ReminderFormView: View { // TODO: This should be on this branch on 'main' try ReminderTag.where { $0.reminderID == reminderID }.delete().execute(db) } else { - updatedReminderID = try Reminder.insert(reminder).returning(\.id).fetchOne(db)!.id + updatedReminderID = try Reminder.insert(reminder).returning(\.id).fetchOne(db)! } try ReminderTag.insert( selectedTags.map { tag in diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 99dc835d..c4d14c10 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -25,9 +25,9 @@ struct RemindersListDetailView: View { case title = "Title" var icon: Image { switch self { - case .dueDate: Image(systemName: "calendar") + case .dueDate: Image(systemName: "calendar") case .priority: Image(systemName: "chart.bar.fill") - case .title: Image(systemName: "textformat.characters") + case .title: Image(systemName: "textformat.characters") } } } @@ -128,47 +128,43 @@ struct RemindersListDetailView: View { ) } - private struct Reminders: FetchKeyRequest { + fileprivate struct Reminders: FetchKeyRequest { let listID: Int64 let ordering: Ordering let showCompleted: Bool func fetch(_ db: Database) throws -> [Record] { - return try Reminder - .where { $0.listID == listID } // TODO: Should `where` return `any Expression` as `@_disfavoredOverload`? // .where { showCompleted ? true : !$0.isCompleted } - .where { showCompleted || !$0.isCompleted } - .group(by: \.id) + // TODO: Do we want to support `buildBlock` in order to fact out `$0.isComplete` + // TODO: Overload to fix + // TODO: Ambiguous '??' + // !$0.0.isCompleted /* && ($0.0.date ?? .raw("date('now')")) < .raw("date('now')") */ + // TODO: tag.groupConcat(by: \.name, separat, ordering: …) + try Reminder + .where { $0.listID == listID } .order { - // TODO: Do we want to support this `buildBlock` - $0.isCompleted - switch ordering { case .dueDate: - $0.date + ($0.isCompleted, $0.date) case .priority: - ($0.priority.descending(), $0.isFlagged.descending()) + ($0.isCompleted, $0.priority.descending(), $0.isFlagged.descending()) case .title: - $0.title + ($0.isCompleted, $0.title) } } - .leftJoin(ReminderTag.all()) { $0.id == $1.reminderID } - // TODO: Overload to fix - .leftJoin(Tag.all()) { $0.1.tagID == $1.id } + .withTags(showCompleted: showCompleted) .select { - ( - $0.0, - $0.0.isCompleted - && .raw("coalesce(\(bind: $0.0.date), date('now')) < date('now')", as: Bool.self), - $1.name.groupConcat(separator: ",") - // TODO: Ambiguous '??' - // !$0.0.isCompleted /* && ($0.0.date ?? .raw("date('now')")) < .raw("date('now')") */ + let (reminder, tag) = ($0.0, $1) + return Record.Columns( + reminder: reminder, + isPastDue: reminder.isPastDue, + commaSeparatedTags: tag.name.groupConcat(separator: ",") ) } .fetchAll(db) - .map(Record.init) } - struct Record: Decodable, FetchableRecord { + @Selection + fileprivate struct Record: Decodable { var reminder: Reminder var isPastDue: Bool var commaSeparatedTags: String? @@ -179,6 +175,21 @@ struct RemindersListDetailView: View { } } +extension SelectProtocol { + func withTags( + showCompleted: Bool + ) + // TODO: Should a `Optional` be a `Table`? Would that allow `SelectOf`? + -> Select<((Reminder.Columns, ReminderTag.Columns), Tag.Columns), ((Reminder, ReminderTag?), Tag?)> + { + self.all() + .where { showCompleted || !$0.isCompleted } + .group(by: \.id) + .leftJoin(ReminderTag.all()) { $0.id == $1.reminderID } + .leftJoin(Tag.all()) { $0.1.tagID == $1.id } + } +} + #Preview { let remindersList = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase(inMemory: true) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 54658eac..fb146fa7 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -29,6 +29,18 @@ struct Reminder: Codable, Equatable, FetchableRecord, Identifiable, MutablePersi mutating func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID } + + static func searching(_ text: String) -> Where { + Self.where { + $0.title.collate(.nocase).contains(text) + || $0.notes.collate(.nocase).contains(text) + } + } +} +extension Reminder.Columns { + var isPastDue: some QueryExpression { + isCompleted && .raw("coalesce(\(date), date('now')) < date('now')") + } } @Table("tags") @@ -220,12 +232,17 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { func createDebugTags() throws { try Tag.insert(\.name) { "car"; "kids"; "someday"; "optional" }.execute(self) - _ = try ReminderTag(reminderID: 1, tagID: 3).inserted(self) - _ = try ReminderTag(reminderID: 1, tagID: 4).inserted(self) - _ = try ReminderTag(reminderID: 2, tagID: 3).inserted(self) - _ = try ReminderTag(reminderID: 2, tagID: 4).inserted(self) - _ = try ReminderTag(reminderID: 4, tagID: 1).inserted(self) - _ = try ReminderTag(reminderID: 4, tagID: 2).inserted(self) + try ReminderTag.insert { + ($0.reminderID, $0.tagID) + } values: { + (1, 3) + (1, 4) + (2, 3) + (2, 4) + (4, 1) + (4, 2) + } + .execute(self) } } #endif diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 4a555214..37ec5d32 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -1,5 +1,6 @@ import IssueReporting import SharingGRDB +import StructuredQueries import SwiftUI struct SearchRemindersView: View { @@ -87,14 +88,18 @@ struct SearchRemindersView: View { private func deleteCompletedReminders(monthsAgo: Int? = nil) { withErrorReporting { try database.write { db in - let baseQuery = searchQueryBase(searchText: searchText) - .filter(Column("isCompleted")) + let baseQuery = Reminder + .searching(searchText) + .where(\.isCompleted) if let monthsAgo { _ = try baseQuery - .filter(Column("date") < "date('now', '-\(monthsAgo) months')") - .deleteAll(db) + .where { .raw("\($0.date) < date('now', '-\(monthsAgo) months") } + .delete() + .execute(db) } else { - _ = try baseQuery.deleteAll(db) + _ = try baseQuery + .delete() + .execute(db) } } } @@ -105,62 +110,32 @@ struct SearchRemindersView: View { let searchText: String func fetch(_ db: Database) throws -> Value { - struct LocalRequest: Decodable, FetchableRecord { - var isPastDue: Bool - let reminder: Reminder - let remindersListID: Int64 - let commaSeparatedTags: String? - } - let reminders = try LocalRequest.fetchAll( - db, - SQLRequest(literal: """ - SELECT - "reminders".*, - "remindersLists"."id" AS "remindersListID", - group_concat("tags"."name", ',') AS "commaSeparatedTags", - NOT "isCompleted" AND coalesce("reminders"."date", date('now')) < date('now') AS "isPastDue" - FROM "reminders" - LEFT JOIN "remindersLists" ON "reminders"."listID" = "remindersLists"."id" - LEFT JOIN "remindersTags" ON "reminders"."id" = "remindersTags"."reminderID" - LEFT JOIN "tags" ON "remindersTags"."tagID" = "tags"."id" - WHERE - ( - "reminders"."title" COLLATE NOCASE LIKE \("%\(searchText)%") - OR "reminders"."notes" COLLATE NOCASE LIKE \("%\(searchText)%") + try Value( + completedCount: Reminder.searching(searchText) + .where(\.isCompleted) + .count() + .fetchOne(db) ?? 0, + + reminders: Reminder.searching(searchText) + .order { ($0.isCompleted, $0.date) } + .withTags(showCompleted: showCompletedInSearchResults) + .leftJoin(RemindersList.all()) { $0.0.0.listID == $1.id } + .select { + let (reminder, reminderList, tag) = ($0.0.0, $1, $0.1) + return Value.Reminder.Columns( + isPastDue: reminder.isPastDue, + reminder: reminder, + remindersList: reminderList, + commaSeparatedTags: tag.name.groupConcat() ) - \(sql: showCompletedInSearchResults ? "" : #"AND NOT "reminders"."isCompleted""#) - GROUP BY "reminders"."id" - ORDER BY - "reminders"."isCompleted", "reminders"."date" - """) - ) - - // NB: We are loading lists as a separate query because we are not sure how to join - // "remindersLists" into the above query and decode it into 'State'. Ideally this - // could all be done with a single query. - let remindersLists = try RemindersList - .where { reminders.map(\.remindersListID).contains($0.id) } - .fetchAll(db) - - let completedCount = try searchQueryBase(searchText: searchText) - .filter(Column("isCompleted")) - .fetchCount(db) - - return Value( - completedCount: completedCount, - reminders: reminders.map { reminder in - Value.Reminder( - isPastDue: reminder.isPastDue, - reminder: reminder.reminder, - remindersList: remindersLists.first(where: { $0.id == reminder.reminder.listID} )!, - commaSeparatedTags: reminder.commaSeparatedTags - ) - } + } + .fetchAll(db) ) } struct Value { var completedCount = 0 var reminders: [Reminder] = [] + @Selection struct Reminder { var isPastDue: Bool let reminder: Reminders.Reminder @@ -171,14 +146,6 @@ struct SearchRemindersView: View { } } -private func searchQueryBase(searchText: String) -> QueryInterfaceRequest { - Reminder - .filter( - Column("title").collating(.nocase).like("%\(searchText.lowercased())%") - || Column("notes").collating(.nocase).like("%\(searchText.lowercased())%") - ) -} - #Preview { @Previewable @State var searchText = "take" let _ = try! prepareDependencies { diff --git a/Package.swift b/Package.swift index 63cf0c88..5655924d 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,8 @@ let package = Package( .package(url: "https://github.com/groue/GRDB.swift", from: "7.1.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), + //.package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), + .package(path: "../swift-structured-queries") ], targets: [ .target( diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index cf0a4b83..2da81778 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "069a55c89e06e2b296b3a54847c62c4c87d976f6a89ded8f3f65b69858c833e6", + "originHash" : "81e37bd27051a0971a31f564bae3fb39d33e4ee0623fc01eb3e38467a362abf8", "pins" : [ { "identity" : "combine-schedulers", @@ -127,15 +127,6 @@ "version" : "2.3.0" } }, - { - "identity" : "swift-structured-queries", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-structured-queries", - "state" : { - "branch" : "main", - "revision" : "3dbff6900efb8a453353931035ead0e8e78f2f65" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", From 97fec0174cbbd1d8aaf08fe26060b6080cd96176 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 18 Feb 2025 17:07:27 -0800 Subject: [PATCH 005/171] wip --- Package.swift | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 5655924d..a06d556c 100644 --- a/Package.swift +++ b/Package.swift @@ -28,8 +28,8 @@ let package = Package( .package(url: "https://github.com/groue/GRDB.swift", from: "7.1.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - //.package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), - .package(path: "../swift-structured-queries") + .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), + //.package(path: "../swift-structured-queries") ], targets: [ .target( diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2da81778..a1b135cd 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "81e37bd27051a0971a31f564bae3fb39d33e4ee0623fc01eb3e38467a362abf8", + "originHash" : "a629c0a98ec3cc01495ca5d4d5ed2883218424775fd4376d5bd27b3ce50915d2", "pins" : [ { "identity" : "combine-schedulers", @@ -127,6 +127,15 @@ "version" : "2.3.0" } }, + { + "identity" : "swift-structured-queries", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-structured-queries", + "state" : { + "branch" : "main", + "revision" : "d0cb1f45a6c4b3b09b57cf9420e12b8893e1456b" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", From 0f7cf74ee4b63e2dd3811e08a339dae0bccd6f14 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 18 Feb 2025 17:36:03 -0800 Subject: [PATCH 006/171] select only color for search query --- Examples/Examples.xcodeproj/project.pbxproj | 8 +++++++ Examples/Reminders/ReminderRow.swift | 25 ++++++++++++++------ Examples/Reminders/RemindersListDetail.swift | 2 +- Examples/Reminders/Schema.swift | 2 +- Examples/Reminders/SearchReminders.swift | 6 ++--- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 35b61f4a..8984d5e0 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA2908C82D4AF70E003F165F /* UIKitNavigation */; }; + CABAEE392D6567CC004BB89E /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CABAEE382D6567CC004BB89E /* SwiftUINavigation */; }; CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAFDD6492D5E823A00EE099E /* SharingGRDB */; }; DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = DC5FA7472D4C63D60082743E /* DependenciesMacros */; }; DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A132D4842BF0071F499 /* CasePaths */; }; @@ -115,6 +116,7 @@ files = ( CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */, DCDCB67A2D64648C0038EB37 /* StructuredQueriesGRDB in Frameworks */, + CABAEE392D6567CC004BB89E /* SwiftUINavigation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -232,6 +234,7 @@ packageProductDependencies = ( CAFDD6492D5E823A00EE099E /* SharingGRDB */, DCDCB6792D64648C0038EB37 /* StructuredQueriesGRDB */, + CABAEE382D6567CC004BB89E /* SwiftUINavigation */, ); productName = Reminders; productReference = CAF836D82D4735AB0047AEB5 /* Reminders.app */; @@ -790,6 +793,11 @@ package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; productName = UIKitNavigation; }; + CABAEE382D6567CC004BB89E /* SwiftUINavigation */ = { + isa = XCSwiftPackageProductDependency; + package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; + productName = SwiftUINavigation; + }; CAFDD6492D5E823A00EE099E /* SharingGRDB */ = { isa = XCSwiftPackageProductDependency; productName = SharingGRDB; diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 69d57e62..35992c93 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -2,14 +2,15 @@ import Dependencies // TODO: Comment this out and the error messages are bad import StructuredQueriesGRDB import SwiftUI +import SwiftUINavigation struct ReminderRow: View { let isPastDue: Bool let reminder: Reminder - let remindersList: RemindersList + let remindersListColor: Int let tags: [String] - @State var editReminder: Reminder? + @State var editReminder: (Reminder, RemindersList)? @Dependency(\.defaultDatabase) private var database @@ -45,7 +46,7 @@ struct ReminderRow: View { .foregroundStyle(.orange) } Button { - editReminder = reminder + presentEditReminder() } label: { Image(systemName: "info.circle") } @@ -75,16 +76,26 @@ struct ReminderRow: View { } .tint(.orange) Button("Details") { - editReminder = reminder + presentEditReminder() } } - .sheet(item: $editReminder) { reminder in + .sheet(item: $editReminder, id: \.0.id) { reminder, remindersList in NavigationStack { ReminderFormView(existingReminder: reminder, remindersList: remindersList) } } } + private func presentEditReminder() { + withErrorReporting { + try database.read { db in + // TODO: try RemindersList.find(reminder.listID)? + let remindersList = try RemindersList.where { $0.id == reminder.listID }.fetchOne(db) + editReminder = (reminder, remindersList!) + } + } + } + private func completeButtonTapped() { withErrorReporting { try database.write { db in @@ -119,7 +130,7 @@ struct ReminderRow: View { + (reminder.priority == nil ? "" : " ") return ( Text(exclamations) - .foregroundStyle(reminder.isCompleted ? .gray : Color.hex(remindersList.color)) + .foregroundStyle(reminder.isCompleted ? .gray : Color.hex(remindersListColor)) + Text(reminder.title) .foregroundStyle(reminder.isCompleted ? .gray : .primary) ) @@ -143,7 +154,7 @@ struct ReminderRow: View { ReminderRow( isPastDue: false, reminder: reminder, - remindersList: reminderList, + remindersListColor: 0xff0000, tags: ["point-free", "adulting"] ) } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index c4d14c10..cc835023 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -57,7 +57,7 @@ struct RemindersListDetailView: View { ReminderRow( isPastDue: reminderState.isPastDue, reminder: reminderState.reminder, - remindersList: remindersList, + remindersListColor: remindersList.color, tags: reminderState.tags ) } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index fb146fa7..5d4ba072 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -21,7 +21,7 @@ struct Reminder: Codable, Equatable, FetchableRecord, Identifiable, MutablePersi var date: Date? var isCompleted = false var isFlagged = false - var listID: Int64 + var listID: Int64 // TODO: rename to remindersListID? var notes = "" var priority: Int? var title = "" diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 37ec5d32..5baf4f31 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -64,7 +64,7 @@ struct SearchRemindersView: View { ReminderRow( isPastDue: reminder.isPastDue, reminder: reminder.reminder, - remindersList: reminder.remindersList, + remindersListColor: reminder.remindersListColor, tags: (reminder.commaSeparatedTags ?? "").split(separator: ",").map(String.init) ) } @@ -125,7 +125,7 @@ struct SearchRemindersView: View { return Value.Reminder.Columns( isPastDue: reminder.isPastDue, reminder: reminder, - remindersList: reminderList, + remindersListColor: reminderList.color, commaSeparatedTags: tag.name.groupConcat() ) } @@ -139,7 +139,7 @@ struct SearchRemindersView: View { struct Reminder { var isPastDue: Bool let reminder: Reminders.Reminder - let remindersList: RemindersList + let remindersListColor: Int let commaSeparatedTags: String? } } From 2bd94ccf84540adfb0491259ecedfa3350935406 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 18 Feb 2025 17:36:11 -0800 Subject: [PATCH 007/171] Revert "select only color for search query" This reverts commit 0f7cf74ee4b63e2dd3811e08a339dae0bccd6f14. --- Examples/Examples.xcodeproj/project.pbxproj | 8 ------- Examples/Reminders/ReminderRow.swift | 25 ++++++-------------- Examples/Reminders/RemindersListDetail.swift | 2 +- Examples/Reminders/Schema.swift | 2 +- Examples/Reminders/SearchReminders.swift | 6 ++--- 5 files changed, 12 insertions(+), 31 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 8984d5e0..35b61f4a 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA2908C82D4AF70E003F165F /* UIKitNavigation */; }; - CABAEE392D6567CC004BB89E /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CABAEE382D6567CC004BB89E /* SwiftUINavigation */; }; CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAFDD6492D5E823A00EE099E /* SharingGRDB */; }; DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = DC5FA7472D4C63D60082743E /* DependenciesMacros */; }; DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A132D4842BF0071F499 /* CasePaths */; }; @@ -116,7 +115,6 @@ files = ( CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */, DCDCB67A2D64648C0038EB37 /* StructuredQueriesGRDB in Frameworks */, - CABAEE392D6567CC004BB89E /* SwiftUINavigation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -234,7 +232,6 @@ packageProductDependencies = ( CAFDD6492D5E823A00EE099E /* SharingGRDB */, DCDCB6792D64648C0038EB37 /* StructuredQueriesGRDB */, - CABAEE382D6567CC004BB89E /* SwiftUINavigation */, ); productName = Reminders; productReference = CAF836D82D4735AB0047AEB5 /* Reminders.app */; @@ -793,11 +790,6 @@ package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; productName = UIKitNavigation; }; - CABAEE382D6567CC004BB89E /* SwiftUINavigation */ = { - isa = XCSwiftPackageProductDependency; - package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; - productName = SwiftUINavigation; - }; CAFDD6492D5E823A00EE099E /* SharingGRDB */ = { isa = XCSwiftPackageProductDependency; productName = SharingGRDB; diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 35992c93..69d57e62 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -2,15 +2,14 @@ import Dependencies // TODO: Comment this out and the error messages are bad import StructuredQueriesGRDB import SwiftUI -import SwiftUINavigation struct ReminderRow: View { let isPastDue: Bool let reminder: Reminder - let remindersListColor: Int + let remindersList: RemindersList let tags: [String] - @State var editReminder: (Reminder, RemindersList)? + @State var editReminder: Reminder? @Dependency(\.defaultDatabase) private var database @@ -46,7 +45,7 @@ struct ReminderRow: View { .foregroundStyle(.orange) } Button { - presentEditReminder() + editReminder = reminder } label: { Image(systemName: "info.circle") } @@ -76,26 +75,16 @@ struct ReminderRow: View { } .tint(.orange) Button("Details") { - presentEditReminder() + editReminder = reminder } } - .sheet(item: $editReminder, id: \.0.id) { reminder, remindersList in + .sheet(item: $editReminder) { reminder in NavigationStack { ReminderFormView(existingReminder: reminder, remindersList: remindersList) } } } - private func presentEditReminder() { - withErrorReporting { - try database.read { db in - // TODO: try RemindersList.find(reminder.listID)? - let remindersList = try RemindersList.where { $0.id == reminder.listID }.fetchOne(db) - editReminder = (reminder, remindersList!) - } - } - } - private func completeButtonTapped() { withErrorReporting { try database.write { db in @@ -130,7 +119,7 @@ struct ReminderRow: View { + (reminder.priority == nil ? "" : " ") return ( Text(exclamations) - .foregroundStyle(reminder.isCompleted ? .gray : Color.hex(remindersListColor)) + .foregroundStyle(reminder.isCompleted ? .gray : Color.hex(remindersList.color)) + Text(reminder.title) .foregroundStyle(reminder.isCompleted ? .gray : .primary) ) @@ -154,7 +143,7 @@ struct ReminderRow: View { ReminderRow( isPastDue: false, reminder: reminder, - remindersListColor: 0xff0000, + remindersList: reminderList, tags: ["point-free", "adulting"] ) } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index cc835023..c4d14c10 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -57,7 +57,7 @@ struct RemindersListDetailView: View { ReminderRow( isPastDue: reminderState.isPastDue, reminder: reminderState.reminder, - remindersListColor: remindersList.color, + remindersList: remindersList, tags: reminderState.tags ) } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 5d4ba072..fb146fa7 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -21,7 +21,7 @@ struct Reminder: Codable, Equatable, FetchableRecord, Identifiable, MutablePersi var date: Date? var isCompleted = false var isFlagged = false - var listID: Int64 // TODO: rename to remindersListID? + var listID: Int64 var notes = "" var priority: Int? var title = "" diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 5baf4f31..37ec5d32 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -64,7 +64,7 @@ struct SearchRemindersView: View { ReminderRow( isPastDue: reminder.isPastDue, reminder: reminder.reminder, - remindersListColor: reminder.remindersListColor, + remindersList: reminder.remindersList, tags: (reminder.commaSeparatedTags ?? "").split(separator: ",").map(String.init) ) } @@ -125,7 +125,7 @@ struct SearchRemindersView: View { return Value.Reminder.Columns( isPastDue: reminder.isPastDue, reminder: reminder, - remindersListColor: reminderList.color, + remindersList: reminderList, commaSeparatedTags: tag.name.groupConcat() ) } @@ -139,7 +139,7 @@ struct SearchRemindersView: View { struct Reminder { var isPastDue: Bool let reminder: Reminders.Reminder - let remindersListColor: Int + let remindersList: RemindersList let commaSeparatedTags: String? } } From 2976e42ce806d74c71b3b993213db90f8b50f4d2 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 18 Feb 2025 17:37:04 -0800 Subject: [PATCH 008/171] wip --- Examples/Reminders/Schema.swift | 2 +- .../StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index fb146fa7..e0a18ffd 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -21,7 +21,7 @@ struct Reminder: Codable, Equatable, FetchableRecord, Identifiable, MutablePersi var date: Date? var isCompleted = false var isFlagged = false - var listID: Int64 + var listID: Int64 // TODO: rename to reminderListID? var notes = "" var priority: Int? var title = "" diff --git a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift index 643a7278..12c3ed16 100644 --- a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift +++ b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift @@ -3,6 +3,8 @@ import Foundation @_exported import StructuredQueriesCore extension StructuredQueriesCore.Statement { + // TODO: Support try Record.find(reminder.listID)? + public func execute(_ db: Database) throws { let query = queryFragment guard !query.isEmpty else { return } From d819dc785091108208757ff74f10d7c8f2fd1a80 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 19 Feb 2025 08:17:49 -0800 Subject: [PATCH 009/171] wip --- Examples/Reminders/RemindersListDetail.swift | 13 ++++++------- Examples/Reminders/SearchReminders.swift | 13 ++++++------- .../xcshareddata/swiftpm/Package.resolved | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index c4d14c10..b6cde079 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -154,11 +154,10 @@ struct RemindersListDetailView: View { } .withTags(showCompleted: showCompleted) .select { - let (reminder, tag) = ($0.0, $1) - return Record.Columns( - reminder: reminder, - isPastDue: reminder.isPastDue, - commaSeparatedTags: tag.name.groupConcat(separator: ",") + Record.Columns( + reminder: $0, + isPastDue: $0.isPastDue, + commaSeparatedTags: $2.name.groupConcat(separator: ",") ) } .fetchAll(db) @@ -180,13 +179,13 @@ extension SelectProtocol { showCompleted: Bool ) // TODO: Should a `Optional
` be a `Table`? Would that allow `SelectOf`? - -> Select<((Reminder.Columns, ReminderTag.Columns), Tag.Columns), ((Reminder, ReminderTag?), Tag?)> + -> Select<(Reminder.Columns, ReminderTag.Columns, Tag.Columns), (Reminder, ReminderTag?, Tag?)> { self.all() .where { showCompleted || !$0.isCompleted } .group(by: \.id) .leftJoin(ReminderTag.all()) { $0.id == $1.reminderID } - .leftJoin(Tag.all()) { $0.1.tagID == $1.id } + .leftJoin(Tag.all()) { $1.tagID == $2.id } } } diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 37ec5d32..04819152 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -119,14 +119,13 @@ struct SearchRemindersView: View { reminders: Reminder.searching(searchText) .order { ($0.isCompleted, $0.date) } .withTags(showCompleted: showCompletedInSearchResults) - .leftJoin(RemindersList.all()) { $0.0.0.listID == $1.id } + .leftJoin(RemindersList.all()) { $0.listID == $3.id } .select { - let (reminder, reminderList, tag) = ($0.0.0, $1, $0.1) - return Value.Reminder.Columns( - isPastDue: reminder.isPastDue, - reminder: reminder, - remindersList: reminderList, - commaSeparatedTags: tag.name.groupConcat() + Value.Reminder.Columns( + isPastDue: $0.isPastDue, + reminder: $0, + remindersList: $3, + commaSeparatedTags: $2.name.groupConcat() ) } .fetchAll(db) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index a1b135cd..20579f1b 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "d0cb1f45a6c4b3b09b57cf9420e12b8893e1456b" + "revision" : "ecef33d812b437c10442275fc076551c9ce1c5ca" } }, { From baa3e472d3658452d2b8a52515ab65fc5288b762 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 19 Feb 2025 23:21:55 -0800 Subject: [PATCH 010/171] wip --- Examples/Reminders/ReminderForm.swift | 1 + Examples/Reminders/RemindersListDetail.swift | 20 +++++-------------- .../xcshareddata/swiftpm/Package.resolved | 2 +- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index e4c4f4ce..0966b1e7 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -214,6 +214,7 @@ extension Reminder.Draft { set { date = newValue ? Date() : nil } } } + extension Optional { fileprivate subscript(coalesce coalesce: Wrapped) -> Wrapped { get { self ?? coalesce } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index b6cde079..b50a44f4 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -133,13 +133,8 @@ struct RemindersListDetailView: View { let ordering: Ordering let showCompleted: Bool func fetch(_ db: Database) throws -> [Record] { - // TODO: Should `where` return `any Expression` as `@_disfavoredOverload`? - // .where { showCompleted ? true : !$0.isCompleted } // TODO: Do we want to support `buildBlock` in order to fact out `$0.isComplete` - // TODO: Overload to fix - // TODO: Ambiguous '??' - // !$0.0.isCompleted /* && ($0.0.date ?? .raw("date('now')")) < .raw("date('now')") */ - // TODO: tag.groupConcat(by: \.name, separat, ordering: …) + // TODO: tag.groupConcat(by: \.name, separator: ",", ordering: …) try Reminder .where { $0.listID == listID } .order { @@ -157,7 +152,7 @@ struct RemindersListDetailView: View { Record.Columns( reminder: $0, isPastDue: $0.isPastDue, - commaSeparatedTags: $2.name.groupConcat(separator: ",") + commaSeparatedTags: $2.name.groupConcat() ) } .fetchAll(db) @@ -174,14 +169,9 @@ struct RemindersListDetailView: View { } } -extension SelectProtocol { - func withTags( - showCompleted: Bool - ) - // TODO: Should a `Optional
` be a `Table`? Would that allow `SelectOf`? - -> Select<(Reminder.Columns, ReminderTag.Columns, Tag.Columns), (Reminder, ReminderTag?, Tag?)> - { - self.all() +extension SelectStatementOf { + func withTags(showCompleted: Bool) -> SelectOf { + all() .where { showCompleted || !$0.isCompleted } .group(by: \.id) .leftJoin(ReminderTag.all()) { $0.id == $1.reminderID } diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 20579f1b..2326671a 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "ecef33d812b437c10442275fc076551c9ce1c5ca" + "revision" : "887511da9d6b3edcdfffd394d7c56e2afe85a000" } }, { From 0e0c250bbeeb5d2dd3aa7aa21540a52ba41358b1 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 20 Feb 2025 12:34:46 -0800 Subject: [PATCH 011/171] wip --- Examples/Reminders/RemindersListDetail.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index b50a44f4..09a8956f 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -3,12 +3,6 @@ import SharingGRDB import StructuredQueriesGRDB import SwiftUI -extension OrderingBuilder { - public static func buildBlock(_ component: [OrderingTerm]...) -> [OrderingTerm] { - component.flatMap { $0 } - } -} - struct RemindersListDetailView: View { @State.SharedReader private var remindersState: [Reminders.Record] @Shared private var ordering: Ordering @@ -133,8 +127,6 @@ struct RemindersListDetailView: View { let ordering: Ordering let showCompleted: Bool func fetch(_ db: Database) throws -> [Record] { - // TODO: Do we want to support `buildBlock` in order to fact out `$0.isComplete` - // TODO: tag.groupConcat(by: \.name, separator: ",", ordering: …) try Reminder .where { $0.listID == listID } .order { From 86b73f461ab228e88a95877e434a637a35914774 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 20 Feb 2025 17:02:48 -0800 Subject: [PATCH 012/171] clean up --- Examples/Reminders/RemindersListDetail.swift | 115 ++++++++----------- Examples/Reminders/Schema.swift | 29 ++--- Examples/Reminders/SearchReminders.swift | 93 ++++++--------- 3 files changed, 92 insertions(+), 145 deletions(-) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 09a8956f..b17726c7 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -4,50 +4,25 @@ import StructuredQueriesGRDB import SwiftUI struct RemindersListDetailView: View { - @State.SharedReader private var remindersState: [Reminders.Record] + @State.SharedReader(value: []) private var reminderStates: [ReminderState] + @State private var isNewReminderSheetPresented = false @Shared private var ordering: Ordering @Shared private var showCompleted: Bool private let remindersList: RemindersList - @State var isNewReminderSheetPresented = false - @Dependency(\.defaultDatabase) private var database - enum Ordering: String, CaseIterable { - case dueDate = "Due Date" - case priority = "Priority" - case title = "Title" - var icon: Image { - switch self { - case .dueDate: Image(systemName: "calendar") - case .priority: Image(systemName: "chart.bar.fill") - case .title: Image(systemName: "textformat.characters") - } - } - } - init(remindersList: RemindersList) { self.remindersList = remindersList - _remindersState = State.SharedReader(value: []) _ordering = Shared(wrappedValue: .dueDate, .appStorage("ordering_list_\(remindersList.id)")) _showCompleted = Shared( wrappedValue: false, .appStorage("show_completed_list_\(remindersList.id)") ) - $remindersState = SharedReader( - .fetch( - Reminders( - listID: remindersList.id, - ordering: ordering, - showCompleted: showCompleted - ), - animation: .default - ) - ) } var body: some View { List { - ForEach(remindersState, id: \.reminder.id) { reminderState in + ForEach(reminderStates, id: \.reminder.id) { reminderState in ReminderRow( isPastDue: reminderState.isPastDue, reminder: reminderState.reminder, @@ -113,58 +88,62 @@ struct RemindersListDetailView: View { } } + private enum Ordering: String, CaseIterable { + case dueDate = "Due Date" + case priority = "Priority" + case title = "Title" + var icon: Image { + switch self { + case .dueDate: Image(systemName: "calendar") + case .priority: Image(systemName: "chart.bar.fill") + case .title: Image(systemName: "textformat.characters") + } + } + } + private func updateQuery() async throws { - try await $remindersState.load( - .fetch( - Reminders(listID: remindersList.id, ordering: ordering, showCompleted: showCompleted), + try await $reminderStates.load( + .fetchAll( + Reminder + .where { $0.listID == remindersList.id } + .where { showCompleted || !$0.isCompleted } + .order { + switch ordering { + case .dueDate: + ($0.isCompleted, $0.date) + case .priority: + ($0.isCompleted, $0.priority.descending(), $0.isFlagged.descending()) + case .title: + ($0.isCompleted, $0.title) + } + } + .withTags + .select { + ReminderState.Columns( + reminder: $0, + isPastDue: $0.isPastDue, + commaSeparatedTags: $2.name.groupConcat() + ) + }, animation: .default ) ) } - fileprivate struct Reminders: FetchKeyRequest { - let listID: Int64 - let ordering: Ordering - let showCompleted: Bool - func fetch(_ db: Database) throws -> [Record] { - try Reminder - .where { $0.listID == listID } - .order { - switch ordering { - case .dueDate: - ($0.isCompleted, $0.date) - case .priority: - ($0.isCompleted, $0.priority.descending(), $0.isFlagged.descending()) - case .title: - ($0.isCompleted, $0.title) - } - } - .withTags(showCompleted: showCompleted) - .select { - Record.Columns( - reminder: $0, - isPastDue: $0.isPastDue, - commaSeparatedTags: $2.name.groupConcat() - ) - } - .fetchAll(db) - } - @Selection - fileprivate struct Record: Decodable { - var reminder: Reminder - var isPastDue: Bool - var commaSeparatedTags: String? - var tags: [String] { - (commaSeparatedTags ?? "").split(separator: ",").map(String.init) - } + @Selection + fileprivate struct ReminderState: Decodable { + var reminder: Reminder + var isPastDue: Bool + var commaSeparatedTags: String? + var tags: [String] { + (commaSeparatedTags ?? "").split(separator: ",").map(String.init) } } } extension SelectStatementOf { - func withTags(showCompleted: Bool) -> SelectOf { + var withTags: SelectOf { all() - .where { showCompleted || !$0.isCompleted } .group(by: \.id) .leftJoin(ReminderTag.all()) { $0.id == $1.reminderID } .leftJoin(Tag.all()) { $1.tagID == $2.id } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index e0a18ffd..a891d358 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -4,8 +4,14 @@ import IssueReporting import SharingGRDB import StructuredQueriesGRDB +// TODO: remove once previews are updated +extension RemindersList: FetchableRecord, MutablePersistableRecord {} +extension Reminder: FetchableRecord, MutablePersistableRecord {} +extension Tag: FetchableRecord, MutablePersistableRecord {} +extension ReminderTag: FetchableRecord, MutablePersistableRecord {} + @Table("remindersLists") -struct RemindersList: Codable, FetchableRecord, Hashable, Identifiable, MutablePersistableRecord { +struct RemindersList: Codable, Hashable, Identifiable { static let databaseTableName = "remindersLists" var id: Int64 var color = 0x4a99ef @@ -13,9 +19,7 @@ struct RemindersList: Codable, FetchableRecord, Hashable, Identifiable, MutableP } @Table("reminders") -struct Reminder: Codable, Equatable, FetchableRecord, Identifiable, MutablePersistableRecord { - static let databaseTableName = "reminders" - +struct Reminder: Codable, Equatable, Identifiable { var id: Int64 @Column(as: .iso8601) var date: Date? @@ -25,11 +29,6 @@ struct Reminder: Codable, Equatable, FetchableRecord, Identifiable, MutablePersi var notes = "" var priority: Int? var title = "" - - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } - static func searching(_ text: String) -> Where { Self.where { $0.title.collate(.nocase).contains(text) @@ -44,21 +43,13 @@ extension Reminder.Columns { } @Table("tags") -struct Tag: Codable, FetchableRecord, MutablePersistableRecord { - static let databaseTableName = "tags" - +struct Tag: Codable { var id: Int64 var name = "" - - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } @Table("remindersTags") -struct ReminderTag: Codable, FetchableRecord, MutablePersistableRecord { - static let databaseTableName = "remindersTags" - +struct ReminderTag: Codable { // TODO: Both of these should be non-optional even on 'main' var reminderID: Int64 var tagID: Int64 diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 04819152..9fb385ab 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -4,8 +4,8 @@ import StructuredQueries import SwiftUI struct SearchRemindersView: View { - @State.SharedReader(value: SearchReminders.Value()) var searchReminders - + @State.SharedReader(value: 0) var completedCount: Int + @State.SharedReader(value: []) var reminders: [ReminderState] let searchText: String @State var showCompletedInSearchResults = false @@ -13,24 +13,14 @@ struct SearchRemindersView: View { init(searchText: String) { self.searchText = searchText - $searchReminders = SharedReader( - wrappedValue: searchReminders, - .fetch( - SearchReminders( - showCompletedInSearchResults: showCompletedInSearchResults, - searchText: searchText - ), - animation: .default - ) - ) } var body: some View { HStack { - Text("\(searchReminders.completedCount) Completed") + Text("\(completedCount) Completed") .monospacedDigit() .contentTransition(.numericText()) - if searchReminders.completedCount > 0 { + if completedCount > 0 { Text("•") Menu { Text("Clear Completed Reminders") @@ -60,7 +50,7 @@ struct SearchRemindersView: View { } } - ForEach(searchReminders.reminders, id: \.reminder.id) { reminder in + ForEach(reminders) { reminder in ReminderRow( isPastDue: reminder.isPastDue, reminder: reminder.reminder, @@ -74,12 +64,29 @@ struct SearchRemindersView: View { if searchText.isEmpty { showCompletedInSearchResults = false } - try await $searchReminders.load( - .fetch( - SearchReminders( - showCompletedInSearchResults: showCompletedInSearchResults, - searchText: searchText - ), + try await $completedCount.load( + .fetchOne( + Reminder.searching(searchText) + .where(\.isCompleted) + .count(), + animation: .default + ) + ) + try await $reminders.load( + .fetchAll( + Reminder.searching(searchText) + .where { showCompletedInSearchResults || !$0.isCompleted } + .order { ($0.isCompleted, $0.date) } + .withTags + .leftJoin(RemindersList.all()) { $0.listID == $3.id } + .select { + ReminderState.Columns( + isPastDue: $0.isPastDue, + reminder: $0, + remindersList: $3, + commaSeparatedTags: $2.name.groupConcat() + ) + }, animation: .default ) ) @@ -105,43 +112,13 @@ struct SearchRemindersView: View { } } - struct SearchReminders: FetchKeyRequest { - let showCompletedInSearchResults: Bool - let searchText: String - - func fetch(_ db: Database) throws -> Value { - try Value( - completedCount: Reminder.searching(searchText) - .where(\.isCompleted) - .count() - .fetchOne(db) ?? 0, - - reminders: Reminder.searching(searchText) - .order { ($0.isCompleted, $0.date) } - .withTags(showCompleted: showCompletedInSearchResults) - .leftJoin(RemindersList.all()) { $0.listID == $3.id } - .select { - Value.Reminder.Columns( - isPastDue: $0.isPastDue, - reminder: $0, - remindersList: $3, - commaSeparatedTags: $2.name.groupConcat() - ) - } - .fetchAll(db) - ) - } - struct Value { - var completedCount = 0 - var reminders: [Reminder] = [] - @Selection - struct Reminder { - var isPastDue: Bool - let reminder: Reminders.Reminder - let remindersList: RemindersList - let commaSeparatedTags: String? - } - } + @Selection + struct ReminderState: Identifiable { + var id: Reminder.ID { reminder.id } + var isPastDue: Bool + let reminder: Reminders.Reminder + let remindersList: RemindersList + let commaSeparatedTags: String? } } From ebcc9e2c586f969ace551328d00d09dbfaa2fea3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 20 Feb 2025 17:18:54 -0800 Subject: [PATCH 013/171] more clean up --- Examples/Reminders/ReminderRow.swift | 6 +-- Examples/Reminders/RemindersListForm.swift | 27 +++++------ Examples/Reminders/RemindersLists.swift | 55 ++++++++++------------ Examples/Reminders/Schema.swift | 20 ++++---- 4 files changed, 49 insertions(+), 59 deletions(-) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 69d57e62..778221cf 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -56,10 +56,8 @@ struct ReminderRow: View { .swipeActions { Button("Delete") { withErrorReporting { - do { - _ = try database.write { db in - try reminder.delete(db) - } + _ = try database.write { db in + try reminder.delete(db) } } } diff --git a/Examples/Reminders/RemindersListForm.swift b/Examples/Reminders/RemindersListForm.swift index 371160d1..6ab0f7ed 100644 --- a/Examples/Reminders/RemindersListForm.swift +++ b/Examples/Reminders/RemindersListForm.swift @@ -29,21 +29,20 @@ struct RemindersListForm: View { ToolbarItem { Button("Save") { withErrorReporting { - do { - try database.write { db in - if let remindersListID { - try RemindersList.update( - RemindersList( - id: remindersListID, - color: remindersList.color, - name: remindersList.name - ) - ) - .execute(db) - } else { - try RemindersList.insert(remindersList).execute(db) - } + try database.write { db in + guard let remindersListID + else { + try RemindersList.insert(remindersList).execute(db) + return } + try RemindersList.update( + RemindersList( + id: remindersListID, + color: remindersList.color, + name: remindersList.name + ) + ) + .execute(db) } } dismiss() diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 836f9751..4c2d0d0d 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -6,7 +6,15 @@ import StructuredQueries import SwiftUI struct RemindersListsView: View { - @SharedReader(.fetch(RemindersLists(), animation: .default)) private var lists + @SharedReader( + .fetchAll( + RemindersList.group(by: \.id) + .join(Reminder.incomplete) { $0.id == $1.listID } + .select { ReminderListState.Columns(reminderCount: $1.id.count(), remindersList: $0) }, + animation: .default + ) + ) + private var lists @SharedReader(.fetch(Stats())) private var stats = Stats.Value() @State private var isAddListPresented = false @@ -105,38 +113,23 @@ struct RemindersListsView: View { .searchable(text: $searchText) } - struct RemindersLists: FetchKeyRequest { - func fetch(_ db: Database) throws -> [Record] { - try RemindersList - .group(by: \.id) - .join(Reminder.where { !$0.isCompleted }) { $0.id == $1.listID } - .select { ($0, $1.id.count()) } - .fetchAll(db) - .map { Record(reminderCount: $1, remindersList: $0) } - } - // TODO: Fix @Selection to support queryfragment changes - struct Record: Decodable, FetchableRecord { - var reminderCount: Int - var remindersList: RemindersList - } + @Selection + fileprivate struct ReminderListState: Decodable, FetchableRecord { + var reminderCount: Int + var remindersList: RemindersList } - private struct Stats: FetchKeyRequest { + fileprivate struct Stats: FetchKeyRequest { func fetch(_ db: Database) throws -> Value { - let todayCount = try Reminder.count() - .where { .raw("date(\(bind: $0.date)) = date('now')") } - .fetchOne(db) ?? 0 - let allCount = try Reminder.fetchCount(db) - let scheduledCount = try Reminder.count() - .where { .raw("date(\(bind: $0.date)) > date('now')") } - .fetchOne(db) ?? 0 - let flaggedCount = try Reminder.where(\.isFlagged).count().fetchOne(db) ?? 0 - let completedCount = try Reminder.where(\.isCompleted).count().fetchOne(db) ?? 0 - return Value( - allCount: allCount, - completedCount: completedCount, - flaggedCount: flaggedCount, - scheduledCount: scheduledCount, - todayCount: todayCount + try Value( + allCount: Reminder.count().fetchOne(db) ?? 0, + completedCount: Reminder.where(\.isCompleted).count().fetchOne(db) ?? 0, + flaggedCount: Reminder.where(\.isFlagged).count().fetchOne(db) ?? 0, + scheduledCount: Reminder.count() + .where { .raw("date(\($0.date)) > date('now')") } + .fetchOne(db) ?? 0, + todayCount: Reminder.count() + .where { .raw("date(\($0.date)) = date('now')") } + .fetchOne(db) ?? 0 ) } struct Value { diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index a891d358..ecf81bf0 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -10,15 +10,14 @@ extension Reminder: FetchableRecord, MutablePersistableRecord {} extension Tag: FetchableRecord, MutablePersistableRecord {} extension ReminderTag: FetchableRecord, MutablePersistableRecord {} -@Table("remindersLists") +@Table struct RemindersList: Codable, Hashable, Identifiable { - static let databaseTableName = "remindersLists" var id: Int64 var color = 0x4a99ef var name = "" } -@Table("reminders") +@Table struct Reminder: Codable, Equatable, Identifiable { var id: Int64 @Column(as: .iso8601) @@ -35,14 +34,15 @@ struct Reminder: Codable, Equatable, Identifiable { || $0.notes.collate(.nocase).contains(text) } } + static let incomplete = Self.where { !$0.isCompleted } } extension Reminder.Columns { var isPastDue: some QueryExpression { - isCompleted && .raw("coalesce(\(date), date('now')) < date('now')") + !isCompleted && .raw("coalesce(\(date), date('now')) < date('now')") } } -@Table("tags") +@Table struct Tag: Codable { var id: Int64 var name = "" @@ -85,7 +85,7 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { } } migrator.registerMigration("Add reminders table") { db in - try db.create(table: Reminder.databaseTableName) { table in + try db.create(table: Reminder.name) { table in table.autoIncrementedPrimaryKey("id") table.column("date", .date) table.column("isCompleted", .boolean).defaults(to: false).notNull() @@ -99,15 +99,15 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { } } migrator.registerMigration("Add tags table") { db in - try db.create(table: Tag.databaseTableName) { table in + try db.create(table: Tag.name) { table in table.autoIncrementedPrimaryKey("id") table.column("name", .text).notNull().collate(.nocase).unique() } - try db.create(table: ReminderTag.databaseTableName) { table in + try db.create(table: ReminderTag.name) { table in table.column("reminderID", .integer).notNull() - .references(Reminder.databaseTableName, column: "id", onDelete: .cascade) + .references(Reminder.name, column: "id", onDelete: .cascade) table.column("tagID", .integer).notNull() - .references(Tag.databaseTableName, column: "id", onDelete: .cascade) + .references(Tag.name, column: "id", onDelete: .cascade) } } #if DEBUG From a8ce7f91bdc58905b9bb5e362c140b6edf5c083c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 20 Feb 2025 17:35:43 -0800 Subject: [PATCH 014/171] a little bit more clean up --- Examples/Reminders/ReminderForm.swift | 3 ++- Examples/Reminders/ReminderRow.swift | 10 +++++----- Examples/Reminders/RemindersListDetail.swift | 3 +-- Examples/Reminders/RemindersLists.swift | 7 ++++--- Examples/Reminders/Schema.swift | 3 +++ 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 0966b1e7..33236a66 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -181,7 +181,8 @@ struct ReminderFormView: View { updatedReminderID = try Reminder .where { $0.id == reminderID } .update { - // TODO: $0.date = reminder.date + // TODO: Do we want to improve this? + $0.date = reminder.date.map { .bind($0, as: .iso8601) } $0.isCompleted = reminder.isCompleted $0.isFlagged = reminder.isFlagged $0.listID = reminder.listID diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 778221cf..ce6c94b6 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -1,6 +1,5 @@ import Dependencies -// TODO: Comment this out and the error messages are bad -import StructuredQueriesGRDB +import SharingGRDB import SwiftUI struct ReminderRow: View { @@ -65,9 +64,10 @@ struct ReminderRow: View { Button(reminder.isFlagged ? "Unflag" : "Flag") { withErrorReporting { try database.write { db in - var reminder = reminder - reminder.isFlagged.toggle() - _ = try reminder.saved(db) + try Reminder + .where { $0.id == reminder.id } + .update { $0.isFlagged.toggle() } + .execute(db) } } } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index b17726c7..dbe8e75d 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -105,8 +105,7 @@ struct RemindersListDetailView: View { try await $reminderStates.load( .fetchAll( Reminder - .where { $0.listID == remindersList.id } - .where { showCompleted || !$0.isCompleted } + .where { $0.listID == remindersList.id && (showCompleted || !$0.isCompleted) } .order { switch ordering { case .dueDate: diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 4c2d0d0d..95b91612 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -14,7 +14,7 @@ struct RemindersListsView: View { animation: .default ) ) - private var lists + private var remindersLists @SharedReader(.fetch(Stats())) private var stats = Stats.Value() @State private var isAddListPresented = false @@ -68,7 +68,7 @@ struct RemindersListsView: View { .buttonStyle(.plain) Section { - ForEach(lists, id: \.remindersList.id) { state in + ForEach(remindersLists) { state in NavigationLink { RemindersListDetailView(remindersList: state.remindersList) } label: { @@ -114,7 +114,8 @@ struct RemindersListsView: View { } @Selection - fileprivate struct ReminderListState: Decodable, FetchableRecord { + fileprivate struct ReminderListState: Decodable, Identifiable { + var id: RemindersList.ID { remindersList.id } var reminderCount: Int var remindersList: RemindersList } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index ecf81bf0..3bf71323 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -97,6 +97,7 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { table.column("priority", .integer) table.column("title", .text).notNull() } + try db.create(indexOn: Reminder.name, columns: [Reminder.columns.listID.name]) } migrator.registerMigration("Add tags table") { db in try db.create(table: Tag.name) { table in @@ -109,6 +110,8 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { table.column("tagID", .integer).notNull() .references(Tag.name, column: "id", onDelete: .cascade) } + try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.reminderID.name]) + try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.tagID.name]) } #if DEBUG migrator.registerMigration("Add mock data") { db in From ae0066b5e55a69ca16d936c3c336b9276a2cb2ee Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 20 Feb 2025 19:13:55 -0800 Subject: [PATCH 015/171] more clean up --- Examples/Reminders/RemindersListDetail.swift | 5 +++-- Examples/Reminders/RemindersListRow.swift | 2 +- Examples/Reminders/RemindersLists.swift | 2 +- Examples/Reminders/SearchReminders.swift | 17 +++++++++-------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index dbe8e75d..47e330d9 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -22,7 +22,7 @@ struct RemindersListDetailView: View { var body: some View { List { - ForEach(reminderStates, id: \.reminder.id) { reminderState in + ForEach(reminderStates) { reminderState in ReminderRow( isPastDue: reminderState.isPastDue, reminder: reminderState.reminder, @@ -130,7 +130,8 @@ struct RemindersListDetailView: View { } @Selection - fileprivate struct ReminderState: Decodable { + fileprivate struct ReminderState: Decodable, Identifiable { + var id: Reminder.ID { reminder.id } var reminder: Reminder var isPastDue: Bool var commaSeparatedTags: String? diff --git a/Examples/Reminders/RemindersListRow.swift b/Examples/Reminders/RemindersListRow.swift index 54410670..30d07fd7 100644 --- a/Examples/Reminders/RemindersListRow.swift +++ b/Examples/Reminders/RemindersListRow.swift @@ -22,7 +22,7 @@ struct RemindersListRow: View { .swipeActions { Button { withErrorReporting { - _ = try database.write { db in + try database.write { db in try RemindersList .where { $0.id == remindersList.id } .delete() diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 95b91612..cd92f5bd 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -6,7 +6,7 @@ import StructuredQueries import SwiftUI struct RemindersListsView: View { - @SharedReader( + @State.SharedReader( .fetchAll( RemindersList.group(by: \.id) .join(Reminder.incomplete) { $0.id == $1.listID } diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 9fb385ab..a91a3a5b 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -55,7 +55,7 @@ struct SearchRemindersView: View { isPastDue: reminder.isPastDue, reminder: reminder.reminder, remindersList: reminder.remindersList, - tags: (reminder.commaSeparatedTags ?? "").split(separator: ",").map(String.init) + tags: reminder.tags ) } } @@ -81,10 +81,10 @@ struct SearchRemindersView: View { .leftJoin(RemindersList.all()) { $0.listID == $3.id } .select { ReminderState.Columns( + commaSeparatedTags: $2.name.groupConcat(), isPastDue: $0.isPastDue, reminder: $0, - remindersList: $3, - commaSeparatedTags: $2.name.groupConcat() + remindersList: $3 ) }, animation: .default @@ -99,14 +99,12 @@ struct SearchRemindersView: View { .searching(searchText) .where(\.isCompleted) if let monthsAgo { - _ = try baseQuery + try baseQuery .where { .raw("\($0.date) < date('now', '-\(monthsAgo) months") } .delete() .execute(db) } else { - _ = try baseQuery - .delete() - .execute(db) + try baseQuery.delete().execute(db) } } } @@ -115,10 +113,13 @@ struct SearchRemindersView: View { @Selection struct ReminderState: Identifiable { var id: Reminder.ID { reminder.id } + let commaSeparatedTags: String? var isPastDue: Bool let reminder: Reminders.Reminder let remindersList: RemindersList - let commaSeparatedTags: String? + var tags: [String] { + (commaSeparatedTags ?? "").split(separator: ",").map(String.init) + } } } From 46c5cb0db52cda51666fdb1963a2f8d3a60a1818 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Feb 2025 10:39:02 -0800 Subject: [PATCH 016/171] wip --- Examples/Reminders/RemindersListDetail.swift | 50 +++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 47e330d9..54946ec5 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -18,6 +18,7 @@ struct RemindersListDetailView: View { _showCompleted = Shared( wrappedValue: false, .appStorage("show_completed_list_\(remindersList.id)") ) + $reminderStates = SharedReader(wrappedValue: [], remindersKey) } var body: some View { @@ -102,33 +103,36 @@ struct RemindersListDetailView: View { } private func updateQuery() async throws { - try await $reminderStates.load( - .fetchAll( - Reminder - .where { $0.listID == remindersList.id && (showCompleted || !$0.isCompleted) } - .order { - switch ordering { - case .dueDate: - ($0.isCompleted, $0.date) - case .priority: - ($0.isCompleted, $0.priority.descending(), $0.isFlagged.descending()) - case .title: - ($0.isCompleted, $0.title) - } + try await $reminderStates.load(remindersKey) + } + + fileprivate var remindersKey: some SharedReaderKey<[ReminderState]> { + .fetchAll( + Reminder + .where { $0.listID == remindersList.id && (showCompleted || !$0.isCompleted) } + .order { + switch ordering { + case .dueDate: + ($0.isCompleted, $0.date) + case .priority: + ($0.isCompleted, $0.priority.descending(), $0.isFlagged.descending()) + case .title: + ($0.isCompleted, $0.title) } - .withTags - .select { - ReminderState.Columns( - reminder: $0, - isPastDue: $0.isPastDue, - commaSeparatedTags: $2.name.groupConcat() - ) - }, - animation: .default - ) + } + .withTags + .select { + ReminderState.Columns( + reminder: $0, + isPastDue: $0.isPastDue, + commaSeparatedTags: $2.name.groupConcat() + ) + }, + animation: .default ) } + @Selection fileprivate struct ReminderState: Decodable, Identifiable { var id: Reminder.ID { reminder.id } From 16e4238511b31afbfcfb827e81bbe45f9f81e143 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Feb 2025 10:43:22 -0800 Subject: [PATCH 017/171] disable indices for now --- Examples/Examples.xcodeproj/project.pbxproj | 14 +++++++------- Examples/Reminders/Schema.swift | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 35b61f4a..efd63a98 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -8,11 +8,11 @@ /* Begin PBXBuildFile section */ CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA2908C82D4AF70E003F165F /* UIKitNavigation */; }; + CAE6C5EF2D69007E00CE1C90 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAE6C5EE2D69007E00CE1C90 /* StructuredQueriesGRDB */; }; CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAFDD6492D5E823A00EE099E /* SharingGRDB */; }; DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = DC5FA7472D4C63D60082743E /* DependenciesMacros */; }; DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A132D4842BF0071F499 /* CasePaths */; }; DCBE8A162D4842C80071F499 /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A152D4842C80071F499 /* SharingGRDB */; }; - DCDCB67A2D64648C0038EB37 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCDCB6792D64648C0038EB37 /* StructuredQueriesGRDB */; }; DCF267392D48437300B680BE /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = DCF267382D48437300B680BE /* SwiftUINavigation */; }; DCF2684A2D4993BC00B680BE /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCF268492D4993BC00B680BE /* SharingGRDB */; }; /* End PBXBuildFile section */ @@ -114,7 +114,7 @@ buildActionMask = 2147483647; files = ( CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */, - DCDCB67A2D64648C0038EB37 /* StructuredQueriesGRDB in Frameworks */, + CAE6C5EF2D69007E00CE1C90 /* StructuredQueriesGRDB in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -231,7 +231,7 @@ name = Reminders; packageProductDependencies = ( CAFDD6492D5E823A00EE099E /* SharingGRDB */, - DCDCB6792D64648C0038EB37 /* StructuredQueriesGRDB */, + CAE6C5EE2D69007E00CE1C90 /* StructuredQueriesGRDB */, ); productName = Reminders; productReference = CAF836D82D4735AB0047AEB5 /* Reminders.app */; @@ -790,6 +790,10 @@ package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; productName = UIKitNavigation; }; + CAE6C5EE2D69007E00CE1C90 /* StructuredQueriesGRDB */ = { + isa = XCSwiftPackageProductDependency; + productName = StructuredQueriesGRDB; + }; CAFDD6492D5E823A00EE099E /* SharingGRDB */ = { isa = XCSwiftPackageProductDependency; productName = SharingGRDB; @@ -808,10 +812,6 @@ isa = XCSwiftPackageProductDependency; productName = SharingGRDB; }; - DCDCB6792D64648C0038EB37 /* StructuredQueriesGRDB */ = { - isa = XCSwiftPackageProductDependency; - productName = StructuredQueriesGRDB; - }; DCF267382D48437300B680BE /* SwiftUINavigation */ = { isa = XCSwiftPackageProductDependency; package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 3bf71323..a8571721 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -97,7 +97,7 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { table.column("priority", .integer) table.column("title", .text).notNull() } - try db.create(indexOn: Reminder.name, columns: [Reminder.columns.listID.name]) +// try db.create(indexOn: Reminder.name, columns: [Reminder.columns.listID.name]) } migrator.registerMigration("Add tags table") { db in try db.create(table: Tag.name) { table in @@ -110,8 +110,8 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { table.column("tagID", .integer).notNull() .references(Tag.name, column: "id", onDelete: .cascade) } - try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.reminderID.name]) - try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.tagID.name]) +// try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.reminderID.name]) +// try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.tagID.name]) } #if DEBUG migrator.registerMigration("Add mock data") { db in From dc369857c0f6ed4b9072c46c7a2351bd73ec1d2f Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Feb 2025 10:46:53 -0800 Subject: [PATCH 018/171] wip --- Examples/Examples.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index efd63a98..2b99ef57 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA2908C82D4AF70E003F165F /* UIKitNavigation */; }; - CAE6C5EF2D69007E00CE1C90 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAE6C5EE2D69007E00CE1C90 /* StructuredQueriesGRDB */; }; + CAE6C64D2D69017D00CE1C90 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */; }; CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAFDD6492D5E823A00EE099E /* SharingGRDB */; }; DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = DC5FA7472D4C63D60082743E /* DependenciesMacros */; }; DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A132D4842BF0071F499 /* CasePaths */; }; @@ -114,7 +114,7 @@ buildActionMask = 2147483647; files = ( CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */, - CAE6C5EF2D69007E00CE1C90 /* StructuredQueriesGRDB in Frameworks */, + CAE6C64D2D69017D00CE1C90 /* StructuredQueriesGRDB in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -231,7 +231,7 @@ name = Reminders; packageProductDependencies = ( CAFDD6492D5E823A00EE099E /* SharingGRDB */, - CAE6C5EE2D69007E00CE1C90 /* StructuredQueriesGRDB */, + CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */, ); productName = Reminders; productReference = CAF836D82D4735AB0047AEB5 /* Reminders.app */; @@ -790,7 +790,7 @@ package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; productName = UIKitNavigation; }; - CAE6C5EE2D69007E00CE1C90 /* StructuredQueriesGRDB */ = { + CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */ = { isa = XCSwiftPackageProductDependency; productName = StructuredQueriesGRDB; }; From 1cdd0a5c534bf401342eccf89712c02865f7169b Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Feb 2025 11:09:12 -0800 Subject: [PATCH 019/171] wip --- Examples/Reminders/Schema.swift | 6 ++-- Package.swift | 8 ++++- .../xcshareddata/swiftpm/Package.resolved | 4 +-- .../MigrationTests.swift | 36 +++++++++++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 Tests/StructuredQueriesGRDBTests/MigrationTests.swift diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index a8571721..728e0b15 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -74,9 +74,9 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { database = try DatabasePool(path: path, configuration: configuration) } var migrator = DatabaseMigrator() - #if DEBUG - migrator.eraseDatabaseOnSchemaChange = true - #endif +// #if DEBUG +// migrator.eraseDatabaseOnSchemaChange = true +// #endif migrator.registerMigration("Add reminders lists table") { db in try db.create(table: RemindersList.name) { table in table.autoIncrementedPrimaryKey("id") diff --git a/Package.swift b/Package.swift index a06d556c..76c9e539 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,6 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), - //.package(path: "../swift-structured-queries") ], targets: [ .target( @@ -62,6 +61,13 @@ let package = Package( .product(name: "StructuredQueries", package: "swift-structured-queries"), ] ), + .testTarget( + name: "StructuredQueriesGRDBTests", + dependencies: [ + "StructuredQueriesGRDB", + .product(name: "DependenciesTestSupport", package: "swift-dependencies"), + ] + ) ], swiftLanguageModes: [.v6] ) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2326671a..e84e469b 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a629c0a98ec3cc01495ca5d4d5ed2883218424775fd4376d5bd27b3ce50915d2", + "originHash" : "702cf47f7b8527b3bb941563266e3f886871ef870dcc100cb6cde93fcf1c910e", "pins" : [ { "identity" : "combine-schedulers", @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "887511da9d6b3edcdfffd394d7c56e2afe85a000" + "revision" : "a717e07b13c32181a1f216f83af64b1b03121496" } }, { diff --git a/Tests/StructuredQueriesGRDBTests/MigrationTests.swift b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift new file mode 100644 index 00000000..396fa4a1 --- /dev/null +++ b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift @@ -0,0 +1,36 @@ +import Foundation +import GRDB +import StructuredQueriesGRDB +import Testing + +@Suite struct MigrationTests { + @Test func dates() throws { + let database = try DatabaseQueue() + var migrator = DatabaseMigrator() + migrator.registerMigration("Create schema") { db in + try db.create(table: "models") { t in + t.column("date", .datetime).notNull() + } + } + try migrator.migrate(database) + + let timestamp = 123.456 + try database.write { db in + try db.execute( + literal: "INSERT INTO models (date) VALUES (\(Date(timeIntervalSince1970: timestamp)))" + ) + } + try database.read { db in + let grdbDate = try Date.fetchOne(db, sql: "SELECT * FROM models") + try #expect(abs(#require(grdbDate).timeIntervalSince1970 - timestamp) < 0.001) + + let date = try #require(try Model.all().fetchOne(db)).date + try #expect(abs(#require(date).timeIntervalSince1970 - timestamp) < 0.001) + } + } +} + +@Table private struct Model { + @Column(as: .iso8601) + var date: Date +} From ccd619a7ab9f28b894a3579c41a54b8159d5cae3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Feb 2025 19:37:10 -0800 Subject: [PATCH 020/171] wip --- Examples/Reminders/SearchReminders.swift | 37 +++++++++++++----------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index a91a3a5b..483412ea 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -13,6 +13,7 @@ struct SearchRemindersView: View { init(searchText: String) { self.searchText = searchText + $reminders = SharedReader(wrappedValue: [], searchKey) } var body: some View { @@ -72,23 +73,25 @@ struct SearchRemindersView: View { animation: .default ) ) - try await $reminders.load( - .fetchAll( - Reminder.searching(searchText) - .where { showCompletedInSearchResults || !$0.isCompleted } - .order { ($0.isCompleted, $0.date) } - .withTags - .leftJoin(RemindersList.all()) { $0.listID == $3.id } - .select { - ReminderState.Columns( - commaSeparatedTags: $2.name.groupConcat(), - isPastDue: $0.isPastDue, - reminder: $0, - remindersList: $3 - ) - }, - animation: .default - ) + try await $reminders.load(searchKey) + } + + private var searchKey: some SharedReaderKey<[ReminderState]> { + .fetchAll( + Reminder.searching(searchText) + .where { showCompletedInSearchResults || !$0.isCompleted } + .order { ($0.isCompleted, $0.date) } + .withTags + .leftJoin(RemindersList.all()) { $0.listID == $3.id } + .select { + ReminderState.Columns( + commaSeparatedTags: $2.name.groupConcat(), + isPastDue: $0.isPastDue, + reminder: $0, + remindersList: $3 + ) + }, + animation: .default ) } From 65362f16389753280e0db2a2b3d9765c332c116c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Feb 2025 19:37:49 -0800 Subject: [PATCH 021/171] wip --- Examples/Reminders/Schema.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 728e0b15..3bf71323 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -74,9 +74,9 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { database = try DatabasePool(path: path, configuration: configuration) } var migrator = DatabaseMigrator() -// #if DEBUG -// migrator.eraseDatabaseOnSchemaChange = true -// #endif + #if DEBUG + migrator.eraseDatabaseOnSchemaChange = true + #endif migrator.registerMigration("Add reminders lists table") { db in try db.create(table: RemindersList.name) { table in table.autoIncrementedPrimaryKey("id") @@ -97,7 +97,7 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { table.column("priority", .integer) table.column("title", .text).notNull() } -// try db.create(indexOn: Reminder.name, columns: [Reminder.columns.listID.name]) + try db.create(indexOn: Reminder.name, columns: [Reminder.columns.listID.name]) } migrator.registerMigration("Add tags table") { db in try db.create(table: Tag.name) { table in @@ -110,8 +110,8 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { table.column("tagID", .integer).notNull() .references(Tag.name, column: "id", onDelete: .cascade) } -// try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.reminderID.name]) -// try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.tagID.name]) + try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.reminderID.name]) + try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.tagID.name]) } #if DEBUG migrator.registerMigration("Add mock data") { db in From 12eb058a08df0282eacef53a619ef4fbb44bdfb3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Feb 2025 19:40:59 -0800 Subject: [PATCH 022/171] remove mutablerecord conformances --- Examples/Reminders/ReminderForm.swift | 28 ++++++------ Examples/Reminders/ReminderRow.swift | 45 +++++++++++--------- Examples/Reminders/RemindersListDetail.swift | 18 ++++---- Examples/Reminders/Schema.swift | 6 --- 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 33236a66..c3e859a1 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -261,20 +261,20 @@ struct TagsPopover: View { } } -#Preview { - let (remindersList, reminder) = try! prepareDependencies { - $0.defaultDatabase = try Reminders.appDatabase(inMemory: true) - return try $0.defaultDatabase.write { db in - let remindersList = try RemindersList.fetchOne(db)! - return ( - remindersList, - // TODO: Preview bug, use preview provider -// try Reminder.where { $0.listID == remindersList.id }.fetchOne(db)! - try Reminder.filter(Column("listID") == remindersList.id).fetchOne(db)! - ) +struct ReminderFormPreview: PreviewProvider { + static var previews: some View { + let (remindersList, reminder) = try! prepareDependencies { + $0.defaultDatabase = try Reminders.appDatabase(inMemory: true) + return try $0.defaultDatabase.write { db in + let remindersList = try RemindersList.all().fetchOne(db)! + return ( + remindersList, + try Reminder.where { $0.listID == remindersList.id }.fetchOne(db)! + ) + } + } + NavigationStack { + ReminderFormView(existingReminder: reminder, remindersList: remindersList) } - } - NavigationStack { - ReminderFormView(existingReminder: reminder, remindersList: remindersList) } } diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index ce6c94b6..2c8e08f5 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -55,8 +55,11 @@ struct ReminderRow: View { .swipeActions { Button("Delete") { withErrorReporting { - _ = try database.write { db in - try reminder.delete(db) + try database.write { db in + try Reminder + .where { $0.id == reminder.id } + .delete() + .execute(db) } } } @@ -125,25 +128,27 @@ struct ReminderRow: View { } } -#Preview { - var reminder: Reminder! - var reminderList: RemindersList! - let _ = try! prepareDependencies { - $0.defaultDatabase = try Reminders.appDatabase(inMemory: true) - try $0.defaultDatabase.read { db in - reminder = try Reminder.fetchOne(db) - reminderList = try RemindersList.fetchOne(db)! +struct ReminderRowPreview: PreviewProvider { + static var previews: some View { + var reminder: Reminder! + var reminderList: RemindersList! + let _ = try! prepareDependencies { + $0.defaultDatabase = try Reminders.appDatabase(inMemory: true) + try $0.defaultDatabase.read { db in + reminder = try Reminder.all().fetchOne(db) + reminderList = try RemindersList.all().fetchOne(db)! + } } - } - - NavigationStack { - List { - ReminderRow( - isPastDue: false, - reminder: reminder, - remindersList: reminderList, - tags: ["point-free", "adulting"] - ) + + NavigationStack { + List { + ReminderRow( + isPastDue: false, + reminder: reminder, + remindersList: reminderList, + tags: ["point-free", "adulting"] + ) + } } } } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 54946ec5..e329d4c5 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -154,14 +154,16 @@ extension SelectStatementOf { } } -#Preview { - let remindersList = try! prepareDependencies { - $0.defaultDatabase = try Reminders.appDatabase(inMemory: true) - return try $0.defaultDatabase.read { db in - try RemindersList.fetchOne(db)! as RemindersList +struct RemindersListDetailPreview: PreviewProvider { + static var previews: some View { + let remindersList = try! prepareDependencies { + $0.defaultDatabase = try Reminders.appDatabase(inMemory: true) + return try $0.defaultDatabase.read { db in + try RemindersList.all().fetchOne(db)! + } + } + NavigationStack { + RemindersListDetailView(remindersList: remindersList) } - } - NavigationStack { - RemindersListDetailView(remindersList: remindersList) } } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 3bf71323..caf5b52d 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -4,12 +4,6 @@ import IssueReporting import SharingGRDB import StructuredQueriesGRDB -// TODO: remove once previews are updated -extension RemindersList: FetchableRecord, MutablePersistableRecord {} -extension Reminder: FetchableRecord, MutablePersistableRecord {} -extension Tag: FetchableRecord, MutablePersistableRecord {} -extension ReminderTag: FetchableRecord, MutablePersistableRecord {} - @Table struct RemindersList: Codable, Hashable, Identifiable { var id: Int64 From aefb43d33d842e3119962257cf48b0e995d706c8 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Feb 2025 19:53:51 -0800 Subject: [PATCH 023/171] rename column --- Examples/Reminders/ReminderForm.swift | 10 +++---- Examples/Reminders/RemindersListDetail.swift | 2 +- Examples/Reminders/RemindersLists.swift | 2 +- Examples/Reminders/Schema.swift | 28 ++++++++++---------- Examples/Reminders/SearchReminders.swift | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index c3e859a1..3816a9e5 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -25,14 +25,14 @@ struct ReminderFormView: View { date: existingReminder.date, isCompleted: existingReminder.isCompleted, isFlagged: existingReminder.isFlagged, - listID: existingReminder.listID, notes: existingReminder.notes, priority: existingReminder.priority, + remindersListID: existingReminder.remindersListID, title: existingReminder.title ) } else { reminderID = nil - reminder = Reminder.Draft(listID: remindersList.id) + reminder = Reminder.Draft(remindersListID: remindersList.id) } } @@ -126,7 +126,7 @@ struct ReminderFormView: View { } } .onChange(of: remindersList) { - reminder.listID = remindersList.id + reminder.remindersListID = remindersList.id } } } @@ -185,9 +185,9 @@ struct ReminderFormView: View { $0.date = reminder.date.map { .bind($0, as: .iso8601) } $0.isCompleted = reminder.isCompleted $0.isFlagged = reminder.isFlagged - $0.listID = reminder.listID $0.notes = reminder.notes $0.priority = reminder.priority + $0.remindersListID = reminder.remindersListID $0.title = reminder.title } .returning(\.id) @@ -269,7 +269,7 @@ struct ReminderFormPreview: PreviewProvider { let remindersList = try RemindersList.all().fetchOne(db)! return ( remindersList, - try Reminder.where { $0.listID == remindersList.id }.fetchOne(db)! + try Reminder.where { $0.remindersListID == remindersList.id }.fetchOne(db)! ) } } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index e329d4c5..625c9287 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -109,7 +109,7 @@ struct RemindersListDetailView: View { fileprivate var remindersKey: some SharedReaderKey<[ReminderState]> { .fetchAll( Reminder - .where { $0.listID == remindersList.id && (showCompleted || !$0.isCompleted) } + .where { $0.remindersListID == remindersList.id && (showCompleted || !$0.isCompleted) } .order { switch ordering { case .dueDate: diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index cd92f5bd..c260f25b 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -9,7 +9,7 @@ struct RemindersListsView: View { @State.SharedReader( .fetchAll( RemindersList.group(by: \.id) - .join(Reminder.incomplete) { $0.id == $1.listID } + .join(Reminder.incomplete) { $0.id == $1.remindersListID } .select { ReminderListState.Columns(reminderCount: $1.id.count(), remindersList: $0) }, animation: .default ) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index caf5b52d..bc68b57e 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -18,9 +18,9 @@ struct Reminder: Codable, Equatable, Identifiable { var date: Date? var isCompleted = false var isFlagged = false - var listID: Int64 // TODO: rename to reminderListID? var notes = "" var priority: Int? + var remindersListID: Int64 var title = "" static func searching(_ text: String) -> Where { Self.where { @@ -84,14 +84,14 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { table.column("date", .date) table.column("isCompleted", .boolean).defaults(to: false).notNull() table.column("isFlagged", .boolean).defaults(to: false).notNull() - table.column("listID", .integer) + table.column("remindersListID", .integer) .references(RemindersList.name, column: "id", onDelete: .cascade) .notNull() table.column("notes", .text).notNull() table.column("priority", .integer) table.column("title", .text).notNull() } - try db.create(indexOn: Reminder.name, columns: [Reminder.columns.listID.name]) + try db.create(indexOn: Reminder.name, columns: [Reminder.columns.remindersListID.name]) } migrator.registerMigration("Add tags table") { db in try db.create(table: Tag.name) { table in @@ -140,78 +140,78 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { // TODO: Support this? // _ = try Reminder.Draft( // date: Date(), -// listID: 1, // notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", +// remindersListID: 1, // title: "Groceries" // ) // .inserted(self) try Reminder.insert([ Reminder.Draft( date: Date(), - listID: 1, notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", + remindersListID: 1, title: "Groceries" ), Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 2), isFlagged: true, - listID: 1, + remindersListID: 1, title: "Haircut" ), Reminder.Draft( date: Date(), - listID: 1, notes: "Ask about diet", priority: 3, + remindersListID: 1, title: "Doctor appointment" ), Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 190), isCompleted: true, - listID: 1, + remindersListID: 1, title: "Take a walk" ), Reminder.Draft( date: Date(), - listID: 1, + remindersListID: 1, title: "Buy concert tickets" ), Reminder.Draft( date: Date().addingTimeInterval(60 * 60 * 24 * 2), isFlagged: true, - listID: 2, priority: 3, + remindersListID: 2, title: "Pick up kids from school" ), Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, - listID: 2, priority: 1, + remindersListID: 2, title: "Get laundry" ), Reminder.Draft( date: Date().addingTimeInterval(60 * 60 * 24 * 4), isCompleted: false, - listID: 2, priority: 3, + remindersListID: 2, title: "Take out trash" ), Reminder.Draft( date: Date().addingTimeInterval(60 * 60 * 24 * 2), - listID: 3, notes: """ Status of tax return Expenses for next year Changing payroll company """, + remindersListID: 3, title: "Call accountant" ), Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, - listID: 3, priority: 2, + remindersListID: 3, title: "Send weekly emails" ), ]) diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 483412ea..4de75e4f 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -82,7 +82,7 @@ struct SearchRemindersView: View { .where { showCompletedInSearchResults || !$0.isCompleted } .order { ($0.isCompleted, $0.date) } .withTags - .leftJoin(RemindersList.all()) { $0.listID == $3.id } + .leftJoin(RemindersList.all()) { $0.remindersListID == $3.id } .select { ReminderState.Columns( commaSeparatedTags: $2.name.groupConcat(), From 4a9fd222f6d4dfbcdf7fce63e8d5378e3caba665 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 27 Feb 2025 10:19:05 -0800 Subject: [PATCH 024/171] wip --- Examples/Reminders/File.swift | 24 ---------- Examples/Reminders/ReminderForm.swift | 45 +++---------------- .../StructuredQueries/StatementKey.swift | 22 ++++++++- 3 files changed, 26 insertions(+), 65 deletions(-) delete mode 100644 Examples/Reminders/File.swift diff --git a/Examples/Reminders/File.swift b/Examples/Reminders/File.swift deleted file mode 100644 index bf8d0954..00000000 --- a/Examples/Reminders/File.swift +++ /dev/null @@ -1,24 +0,0 @@ -import StructuredQueries -import Foundation - -@Table -struct SyncUp: Codable, Hashable { - var id: Int64? - var isDeleted = false - var seconds = 60 * 5 - var title = "" - static let notDeleted = Self.where { !$0.isDeleted } -} - -@Table -struct Attendee: Codable, Hashable { - var id: Int64? - var isDeleted = false - var name = "" - var syncUpID: Int64 - static let notDeleted = Self.where { !$0.isDeleted } - static let withSyncUp = Attendee.notDeleted - .join(SyncUp.notDeleted) { $0.syncUpID == $1.id } -} - - diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 3816a9e5..b751bc70 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -10,7 +10,6 @@ struct ReminderFormView: View { @State var isPresentingTagsPopover = false @State var remindersList: RemindersList - let reminderID: Reminder.ID? @State var reminder: Reminder.Draft @State var selectedTags: [Tag] = [] @@ -20,18 +19,8 @@ struct ReminderFormView: View { init(existingReminder: Reminder? = nil, remindersList: RemindersList) { self.remindersList = remindersList if let existingReminder { - reminderID = existingReminder.id - reminder = Reminder.Draft( - date: existingReminder.date, - isCompleted: existingReminder.isCompleted, - isFlagged: existingReminder.isFlagged, - notes: existingReminder.notes, - priority: existingReminder.priority, - remindersListID: existingReminder.remindersListID, - title: existingReminder.title - ) + reminder = Reminder.Draft(existingReminder) } else { - reminderID = nil reminder = Reminder.Draft(remindersListID: remindersList.id) } } @@ -131,6 +120,8 @@ struct ReminderFormView: View { } } .task { + guard let reminderID = reminder.id + else { return } do { selectedTags = try await database.read { db in try Tag.all() @@ -169,37 +160,13 @@ struct ReminderFormView: View { private func saveButtonTapped() { withErrorReporting { try database.write { db in - // try reminder.save(db) - let updatedReminderID: Reminder.ID - /* - let updatedReminderID = Reminder.upsert(id: reminderID, reminder).returning(\.id) - - // If Draft had `id?`: - let updatedReminderID = Reminder.upsert(reminder).returning(\.id) - */ - if let reminderID { - updatedReminderID = try Reminder - .where { $0.id == reminderID } - .update { - // TODO: Do we want to improve this? - $0.date = reminder.date.map { .bind($0, as: .iso8601) } - $0.isCompleted = reminder.isCompleted - $0.isFlagged = reminder.isFlagged - $0.notes = reminder.notes - $0.priority = reminder.priority - $0.remindersListID = reminder.remindersListID - $0.title = reminder.title - } - .returning(\.id) - .fetchOne(db)! - // TODO: This should be on this branch on 'main' + let reminderID = try Reminder.upsert(reminder).returning(\.id).fetchOne(db)! + if reminder.id != nil { try ReminderTag.where { $0.reminderID == reminderID }.delete().execute(db) - } else { - updatedReminderID = try Reminder.insert(reminder).returning(\.id).fetchOne(db)! } try ReminderTag.insert( selectedTags.map { tag in - ReminderTag(reminderID: updatedReminderID, tagID: tag.id) + ReminderTag(reminderID: reminderID, tagID: tag.id) } ) .execute(db) diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 11734296..980ff58a 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -108,10 +108,19 @@ private struct FetchAllStatementRequest: FetchKeyReq } static func == (lhs: Self, rhs: Self) -> Bool { - AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // return AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + let lhs = lhs.statement + let rhs = rhs.statement + return AnyHashable(lhs) == AnyHashable(rhs) } func hash(into hasher: inout Hasher) { + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // hasher.combine(statement) + let statement = statement hasher.combine(statement) } } @@ -132,10 +141,19 @@ private struct FetchOneStatementRequest: FetchKeyReq } static func == (lhs: Self, rhs: Self) -> Bool { - AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + let lhs = lhs.statement + let rhs = rhs.statement + return AnyHashable(lhs) == AnyHashable(rhs) } func hash(into hasher: inout Hasher) { + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // hasher.combine(statement) + let statement = statement hasher.combine(statement) } } From ee68b6be97db067d5d49311ae63c80b839dfac32 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 12 Mar 2025 23:49:38 -0700 Subject: [PATCH 025/171] wip --- Examples/Examples.xcodeproj/project.pbxproj | 2 + Examples/Reminders/ReminderForm.swift | 4 +- Examples/Reminders/RemindersListDetail.swift | 15 +-- Examples/Reminders/RemindersLists.swift | 6 +- Examples/Reminders/Schema.swift | 25 ++-- Examples/Reminders/SearchReminders.swift | 35 +++--- .../xcshareddata/swiftpm/Package.resolved | 46 +++---- .../StructuredQueries/StatementKey.swift | 115 +++++++++++------- .../StructuredQueriesGRDBCore.swift | 87 +++++++++---- 9 files changed, 197 insertions(+), 138 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 2b99ef57..48ae692a 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -617,6 +617,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=50"; PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Reminders; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -644,6 +645,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=50"; PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Reminders; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index b751bc70..295a9b6d 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -126,8 +126,8 @@ struct ReminderFormView: View { selectedTags = try await database.read { db in try Tag.all() .order(by: \.name) - .leftJoin(ReminderTag.all()) { $0.id == $1.tagID } - .where { $1.reminderID == reminderID } + .leftJoin(ReminderTag.all()) { $0.id.eq($1.tagID) } + .where { $1.reminderID.eq(reminderID) } .select { tag, _ in tag } .fetchAll(db) } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 625c9287..f6c6e7b0 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -18,7 +18,6 @@ struct RemindersListDetailView: View { _showCompleted = Shared( wrappedValue: false, .appStorage("show_completed_list_\(remindersList.id)") ) - $reminderStates = SharedReader(wrappedValue: [], remindersKey) } var body: some View { @@ -115,7 +114,7 @@ struct RemindersListDetailView: View { case .dueDate: ($0.isCompleted, $0.date) case .priority: - ($0.isCompleted, $0.priority.descending(), $0.isFlagged.descending()) + ($0.isCompleted, $0.priority.desc(), $0.isFlagged.desc()) case .title: ($0.isCompleted, $0.title) } @@ -132,7 +131,6 @@ struct RemindersListDetailView: View { ) } - @Selection fileprivate struct ReminderState: Decodable, Identifiable { var id: Reminder.ID { reminder.id } @@ -145,13 +143,10 @@ struct RemindersListDetailView: View { } } -extension SelectStatementOf { - var withTags: SelectOf { - all() - .group(by: \.id) - .leftJoin(ReminderTag.all()) { $0.id == $1.reminderID } - .leftJoin(Tag.all()) { $1.tagID == $2.id } - } +extension Reminder { + static let withTags = group(by: \.id) + .leftJoin(ReminderTag.all()) { $0.id.eq($1.reminderID) } + .leftJoin(Tag.all()) { $1.tagID.eq($2.id) } } struct RemindersListDetailPreview: PreviewProvider { diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index c260f25b..7ae5ef42 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -9,7 +9,7 @@ struct RemindersListsView: View { @State.SharedReader( .fetchAll( RemindersList.group(by: \.id) - .join(Reminder.incomplete) { $0.id == $1.remindersListID } + .join(Reminder.incomplete) { $0.id.eq($1.remindersListID) } .select { ReminderListState.Columns(reminderCount: $1.id.count(), remindersList: $0) }, animation: .default ) @@ -126,10 +126,10 @@ struct RemindersListsView: View { completedCount: Reminder.where(\.isCompleted).count().fetchOne(db) ?? 0, flaggedCount: Reminder.where(\.isFlagged).count().fetchOne(db) ?? 0, scheduledCount: Reminder.count() - .where { .raw("date(\($0.date)) > date('now')") } + .where { #raw("date(\($0.date)) > date('now')") } .fetchOne(db) ?? 0, todayCount: Reminder.count() - .where { .raw("date(\($0.date)) = date('now')") } + .where { #raw("date(\($0.date)) = date('now')") } .fetchOne(db) ?? 0 ) } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index bc68b57e..6d8b3fdc 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -14,7 +14,7 @@ struct RemindersList: Codable, Hashable, Identifiable { @Table struct Reminder: Codable, Equatable, Identifiable { var id: Int64 - @Column(as: .iso8601) + @Column(as: Date.ISO8601Representation?.self) var date: Date? var isCompleted = false var isFlagged = false @@ -32,7 +32,7 @@ struct Reminder: Codable, Equatable, Identifiable { } extension Reminder.Columns { var isPastDue: some QueryExpression { - !isCompleted && .raw("coalesce(\(date), date('now')) < date('now')") + !isCompleted && #raw("coalesce(\(date), date('now')) < date('now')") } } @@ -44,7 +44,6 @@ struct Tag: Codable { @Table("remindersTags") struct ReminderTag: Codable { - // TODO: Both of these should be non-optional even on 'main' var reminderID: Int64 var tagID: Int64 } @@ -72,40 +71,40 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Add reminders lists table") { db in - try db.create(table: RemindersList.name) { table in + try db.create(table: RemindersList.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("color", .integer).defaults(to: 0x4a99ef).notNull() table.column("name", .text).notNull() } } migrator.registerMigration("Add reminders table") { db in - try db.create(table: Reminder.name) { table in + try db.create(table: Reminder.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("date", .date) table.column("isCompleted", .boolean).defaults(to: false).notNull() table.column("isFlagged", .boolean).defaults(to: false).notNull() table.column("remindersListID", .integer) - .references(RemindersList.name, column: "id", onDelete: .cascade) + .references(RemindersList.tableName, column: "id", onDelete: .cascade) .notNull() table.column("notes", .text).notNull() table.column("priority", .integer) table.column("title", .text).notNull() } - try db.create(indexOn: Reminder.name, columns: [Reminder.columns.remindersListID.name]) + try db.create(indexOn: Reminder.tableName, columns: [Reminder.columns.remindersListID.name]) } migrator.registerMigration("Add tags table") { db in - try db.create(table: Tag.name) { table in + try db.create(table: Tag.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("name", .text).notNull().collate(.nocase).unique() } - try db.create(table: ReminderTag.name) { table in + try db.create(table: ReminderTag.tableName) { table in table.column("reminderID", .integer).notNull() - .references(Reminder.name, column: "id", onDelete: .cascade) + .references(Reminder.tableName, column: "id", onDelete: .cascade) table.column("tagID", .integer).notNull() - .references(Tag.name, column: "id", onDelete: .cascade) + .references(Tag.tableName, column: "id", onDelete: .cascade) } - try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.reminderID.name]) - try db.create(indexOn: ReminderTag.name, columns: [ReminderTag.columns.tagID.name]) + try db.create(indexOn: ReminderTag.tableName, columns: [ReminderTag.columns.reminderID.name]) + try db.create(indexOn: ReminderTag.tableName, columns: [ReminderTag.columns.tagID.name]) } #if DEBUG migrator.registerMigration("Add mock data") { db in diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 4de75e4f..63fe6b4c 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -13,7 +13,6 @@ struct SearchRemindersView: View { init(searchText: String) { self.searchText = searchText - $reminders = SharedReader(wrappedValue: [], searchKey) } var body: some View { @@ -77,22 +76,22 @@ struct SearchRemindersView: View { } private var searchKey: some SharedReaderKey<[ReminderState]> { - .fetchAll( - Reminder.searching(searchText) - .where { showCompletedInSearchResults || !$0.isCompleted } - .order { ($0.isCompleted, $0.date) } - .withTags - .leftJoin(RemindersList.all()) { $0.remindersListID == $3.id } - .select { - ReminderState.Columns( - commaSeparatedTags: $2.name.groupConcat(), - isPastDue: $0.isPastDue, - reminder: $0, - remindersList: $3 - ) - }, - animation: .default - ) + let query = Reminder.searching(searchText) + .where { showCompletedInSearchResults || !$0.isCompleted } + .order { ($0.isCompleted, $0.date) } + .withTags + // TODO: Investigate this failure + // .leftJoin(RemindersList.all()) { $0.remindersListID == $3.id } + .join(RemindersList.all()) { $0.remindersListID.eq($3.id) } + .select { + ReminderState.Columns( + commaSeparatedTags: $2.name.groupConcat(), + isPastDue: $0.isPastDue, + reminder: $0, + remindersList: $3 + ) + } + return .fetchAll(query, animation: .default) } private func deleteCompletedReminders(monthsAgo: Int? = nil) { @@ -103,7 +102,7 @@ struct SearchRemindersView: View { .where(\.isCompleted) if let monthsAgo { try baseQuery - .where { .raw("\($0.date) < date('now', '-\(monthsAgo) months") } + .where { #raw("\($0.date) < date('now', '-\(monthsAgo) months") } .delete() .execute(db) } else { diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index e84e469b..e01f19bd 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", - "version" : "1.0.2" + "revision" : "5928286acce13def418ec36d05a001a9641086f2", + "version" : "1.0.3" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "c7205b172f38439e7ee62c208d1d76fa353c0a81", - "version" : "7.2.0" + "revision" : "6eba24d16952452a8a54f6a639491f3c8215527f", + "version" : "7.3.0" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "e7039aaa4d9cf386fa8324a89f258c3f2c54d751", - "version" : "1.6.0" + "revision" : "19b7263bacb9751f151ec0c93ec816fe1ef67c7b", + "version" : "1.6.1" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", - "version" : "1.0.5" + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "163409ef7dae9d960b87f34b51587b6609a76c1f", - "version" : "1.3.0" + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "52b5e1a09dc016e64ce253e19ab3124b7fae9ac9", - "version" : "1.7.0" + "revision" : "121a428c505c01c4ce02d5ada1c8fc3da93afce9", + "version" : "1.8.0" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", - "version" : "1.1.0" + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "e28911721538fa0c2439e92320bad13e3200866f", - "version" : "2.2.3" + "revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4", + "version" : "2.3.0" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "8d52279b9809ef27eabe7d5420f03734528f19da", - "version" : "1.4.1" + "revision" : "21811d6230a625fa0f2e6ffa85be857075cc02c4", + "version" : "1.5.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-sharing", "state" : { - "revision" : "10ba53dd428aed9fc4a1543d3271860a6d4b8dd2", - "version" : "2.3.0" + "revision" : "2c840cf2ae0526ad6090e7796c4e13d9a2339f4a", + "version" : "2.3.3" } }, { @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "a717e07b13c32181a1f216f83af64b1b03121496" + "revision" : "3a99a1d34c02e8fb4abb8d6b594f62456138ea71" } }, { @@ -150,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "b444594f79844b0d6d76d70fbfb3f7f71728f938", - "version" : "1.5.1" + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" } } ], diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 980ff58a..327cd79e 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -11,82 +11,102 @@ import StructuredQueriesGRDBCore @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { - public static func fetchAll( - _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, + public static func fetchAll( + _ statement: S, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler = .async(onQueue: .main) ) -> Self - where Self == FetchKey<[(repeat each Value)]>.Default { + where + S.QueryValue == (), + S.Joins == (repeat each J), + Self == FetchKey<[(S.From.QueryOutput, repeat (each J).QueryOutput)]>.Default + { + fetch( + FetchAllStatementRequest(statement: statement.selectAll()), + database: database, + scheduler: scheduler + ) + } + + public static func fetchAll( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler = .async(onQueue: .main) + ) -> Self + where + S.QueryValue == (V1, repeat each V2), + Self == FetchKey<[(V1.QueryOutput, repeat (each V2).QueryOutput)]>.Default + { fetch(FetchAllStatementRequest(statement: statement), database: database, scheduler: scheduler) } - public static func fetchAll( - _ statement: some StructuredQueriesCore.Statement<[Value]>, + public static func fetchAll( + _ statement: S, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler = .async(onQueue: .main) ) -> Self - where Self == FetchKey<[Value]>.Default { + where S.QueryValue: QueryRepresentable, Self == FetchKey<[S.QueryValue.QueryOutput]>.Default { fetch(FetchAllStatementRequest(statement: statement), database: database, scheduler: scheduler) } - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler = .async(onQueue: .main) ) -> Self - where Self == FetchKey<(repeat each Value)> { + where Self == FetchKey<(repeat (each Value).QueryOutput)> { fetch(FetchOneStatementRequest(statement: statement), database: database, scheduler: scheduler) } - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement<[Value]>, + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler = .async(onQueue: .main) ) -> Self - where Self == FetchKey { + where Self == FetchKey { fetch(FetchOneStatementRequest(statement: statement), database: database, scheduler: scheduler) } #if canImport(SwiftUI) - public static func fetchAll( - _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, + public static func fetchAll( + _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, animation: Animation ) -> Self - where Self == FetchKey<[(repeat each Value)]>.Default { + where Self == FetchKey<[(repeat (each Value).QueryOutput)]>.Default { fetch( FetchAllStatementRequest(statement: statement), database: database, animation: animation ) } - public static func fetchAll( - _ statement: some StructuredQueriesCore.Statement<[Value]>, + public static func fetchAll( + _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, animation: Animation ) -> Self - where Self == FetchKey<[Value]>.Default { + where Self == FetchKey<[Value.QueryOutput]>.Default { fetch( FetchAllStatementRequest(statement: statement), database: database, animation: animation ) } - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement<[(repeat each Value)]>, + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, animation: Animation ) -> Self - where Self == FetchKey<(repeat each Value)> { + where Self == FetchKey<(repeat (each Value).QueryOutput)> { fetch( FetchOneStatementRequest(statement: statement), database: database, animation: animation ) } - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement<[Value]>, + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, animation: Animation ) -> Self - where Self == FetchKey { + where Self == FetchKey { fetch( FetchOneStatementRequest(statement: statement), database: database, animation: animation ) @@ -95,16 +115,11 @@ extension SharedReaderKey { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -private struct FetchAllStatementRequest: FetchKeyRequest { - let statement: any StructuredQueriesCore.Statement<[(repeat each Value)]> - - func fetch(_ db: Database) throws -> [(repeat each Value)] { - func open( - _ statement: any StructuredQueriesCore.Statement<[(repeat each Value)]> - ) throws -> [(repeat each Value)] { - try statement.fetchAll(db) - } - return try open(statement) +private struct FetchAllStatementRequest: FetchKeyRequest { + let statement: any StructuredQueriesCore.Statement<(repeat each Value)> + + func fetch(_ db: Database) throws -> [(repeat (each Value).QueryOutput)] { + try statement.fetchAll(db) } static func == (lhs: Self, rhs: Self) -> Bool { @@ -126,18 +141,13 @@ private struct FetchAllStatementRequest: FetchKeyReq } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -private struct FetchOneStatementRequest: FetchKeyRequest { - let statement: any StructuredQueriesCore.Statement<[(repeat each Value)]> - - func fetch(_ db: Database) throws -> (repeat each Value) { - func open( - _ statement: any StructuredQueriesCore.Statement<[(repeat each Value)]> - ) throws -> (repeat each Value) { - guard let result = try statement.fetchOne(db) - else { throw NotFound() } - return result - } - return try open(statement) +private struct FetchOneStatementRequest: FetchKeyRequest { + let statement: any StructuredQueriesCore.Statement<(repeat each Value)> + + func fetch(_ db: Database) throws -> (repeat (each Value).QueryOutput) { + guard let result = try statement.fetchOne(db) + else { throw NotFound() } + return result } static func == (lhs: Self, rhs: Self) -> Bool { @@ -157,3 +167,16 @@ private struct FetchOneStatementRequest: FetchKeyReq hasher.combine(statement) } } + +// TODO: Define in Structured Queries? +fileprivate extension SelectStatement where QueryValue == () { + func selectAll< + each J: StructuredQueriesCore.Table + >() -> Select<(From, repeat each J), From, (repeat each J)> + where Joins == (repeat each J) { + unsafeBitCast( + self, + to: Select<(From, repeat each J), From, (repeat each J)>.self + ) + } +} diff --git a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift index 12c3ed16..314381ec 100644 --- a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift +++ b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift @@ -6,70 +6,111 @@ extension StructuredQueriesCore.Statement { // TODO: Support try Record.find(reminder.listID)? public func execute(_ db: Database) throws { - let query = queryFragment + let query = self.query guard !query.isEmpty else { return } try db.execute(sql: query.string, arguments: query.arguments) } - public func fetchAll( + public func fetchAll( _ db: Database - ) throws -> [(repeat each Value)] - where QueryOutput == [(repeat each Value)] { - let query = queryFragment + ) throws -> [(repeat (each Value).QueryOutput)] + where QueryValue == (repeat each Value) { + let query = self.query guard !query.isEmpty else { return [] } let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) - var results: [(repeat each Value)] = [] + var results: [(repeat (each Value).QueryOutput)] = [] let decoder = GRDBQueryDecoder() while let row = try cursor.next() { try decoder.withRow(row) { - try results.append((repeat (each Value)(decoder: decoder))) + try results.append((repeat (each Value)(decoder: decoder).queryOutput)) } } return results } - public func fetchAll( + public func fetchAll( _ db: Database - ) throws -> [Value] - where QueryOutput == [Value] { - let query = queryFragment + ) throws -> [QueryValue.QueryOutput] + where QueryValue: QueryRepresentable { + let query = self.query guard !query.isEmpty else { return [] } let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) - var results: [Value] = [] + var results: [QueryValue.QueryOutput] = [] let decoder = GRDBQueryDecoder() while let row = try cursor.next() { try decoder.withRow(row) { - try results.append(Value(decoder: decoder)) + try results.append(QueryValue(decoder: decoder).queryOutput) } } return results } - public func fetchOne( + public func fetchOne( _ db: Database - ) throws -> (repeat each Value)? - where QueryOutput == [(repeat each Value)] { - let query = queryFragment + ) throws -> (repeat (each Value).QueryOutput)? + where QueryValue == (repeat each Value) { + let query = self.query guard !query.isEmpty else { return nil } let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) guard let row = try cursor.next() else { return nil } let decoder = GRDBQueryDecoder() return try decoder.withRow(row) { - try (repeat (each Value)(decoder: decoder)) + try (repeat (each Value)(decoder: decoder).queryOutput) } } - public func fetchOne( + public func fetchOne( _ db: Database - ) throws -> Value? - where QueryOutput == [Value] { - let query = queryFragment + ) throws -> QueryValue.QueryOutput? + where QueryValue: QueryRepresentable { + let query = self.query guard !query.isEmpty else { return nil } let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) guard let row = try cursor.next() else { return nil } let decoder = GRDBQueryDecoder() return try decoder.withRow(row) { - try Value(decoder: decoder) + try QueryValue(decoder: decoder).queryOutput + } + } +} + +extension SelectStatement where QueryValue == () { + public func fetchAll( + _ db: Database + ) throws -> [(From.QueryOutput, repeat (each J).QueryOutput)] + where Joins == (repeat each J) { + let query = self.query + guard !query.isEmpty else { return [] } + let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) + var results: [(From.QueryOutput, repeat (each J).QueryOutput)] = [] + let decoder = GRDBQueryDecoder() + while let row = try cursor.next() { + try decoder.withRow(row) { + try results.append( + ( + From(decoder: decoder).queryOutput, + repeat (each J)(decoder: decoder).queryOutput + ) + ) + } + } + return results + } + + public func fetchOne( + _ db: Database + ) throws -> (From.QueryOutput, repeat (each J).QueryOutput)? + where Joins == (repeat each J) { + let query = self.query + guard !query.isEmpty else { return nil } + let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) + guard let row = try cursor.next() else { return nil } + let decoder = GRDBQueryDecoder() + return try decoder.withRow(row) { + try ( + From(decoder: decoder).queryOutput, + repeat (each J)(decoder: decoder).queryOutput + ) } } } From 6965e4e9f4761d1841d5d16f64971233598a8dc8 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 14 Mar 2025 15:20:46 -0700 Subject: [PATCH 026/171] Convert SyncUps to StructuredQueries. --- Examples/Examples.xcodeproj/project.pbxproj | 7 ++ Examples/SyncUps/App.swift | 2 +- Examples/SyncUps/RecordMeeting.swift | 12 +- Examples/SyncUps/Schema.swift | 106 ++++++++++-------- Examples/SyncUps/SyncUpDetail.swift | 27 +++-- Examples/SyncUps/SyncUpForm.swift | 25 +++-- Examples/SyncUps/SyncUpsList.swift | 32 +++--- .../xcshareddata/swiftpm/Package.resolved | 2 +- 8 files changed, 122 insertions(+), 91 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 48ae692a..0ca2d452 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA2908C82D4AF70E003F165F /* UIKitNavigation */; }; CAE6C64D2D69017D00CE1C90 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */; }; + CAF3EAB92D84D85400E7E0D0 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAF3EAB82D84D85400E7E0D0 /* StructuredQueriesGRDB */; }; CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAFDD6492D5E823A00EE099E /* SharingGRDB */; }; DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = DC5FA7472D4C63D60082743E /* DependenciesMacros */; }; DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A132D4842BF0071F499 /* CasePaths */; }; @@ -122,6 +123,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CAF3EAB92D84D85400E7E0D0 /* StructuredQueriesGRDB in Frameworks */, DCF267392D48437300B680BE /* SwiftUINavigation in Frameworks */, DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */, DCBE8A162D4842C80071F499 /* SharingGRDB in Frameworks */, @@ -258,6 +260,7 @@ DCBE8A152D4842C80071F499 /* SharingGRDB */, DCF267382D48437300B680BE /* SwiftUINavigation */, DC5FA7472D4C63D60082743E /* DependenciesMacros */, + CAF3EAB82D84D85400E7E0D0 /* StructuredQueriesGRDB */, ); productName = SyncUps; productReference = DCBE89CC2D483FB90071F499 /* SyncUps.app */; @@ -796,6 +799,10 @@ isa = XCSwiftPackageProductDependency; productName = StructuredQueriesGRDB; }; + CAF3EAB82D84D85400E7E0D0 /* StructuredQueriesGRDB */ = { + isa = XCSwiftPackageProductDependency; + productName = StructuredQueriesGRDB; + }; CAFDD6492D5E823A00EE099E /* SharingGRDB */ = { isa = XCSwiftPackageProductDependency; productName = SharingGRDB; diff --git a/Examples/SyncUps/App.swift b/Examples/SyncUps/App.swift index 9172ba90..d94f714c 100644 --- a/Examples/SyncUps/App.swift +++ b/Examples/SyncUps/App.swift @@ -80,7 +80,7 @@ struct AppView: View { #Preview("Happy path") { let _ = prepareDependencies { - $0.defaultDatabase = SyncUps.appDatabase(inMemory: true) + $0.defaultDatabase = try! SyncUps.appDatabase(inMemory: true) } AppView(model: AppModel()) } diff --git a/Examples/SyncUps/RecordMeeting.swift b/Examples/SyncUps/RecordMeeting.swift index c1d05ab4..4cd59314 100644 --- a/Examples/SyncUps/RecordMeeting.swift +++ b/Examples/SyncUps/RecordMeeting.swift @@ -124,8 +124,10 @@ final class RecordMeetingModel: HashableObject { try? await clock.sleep(for: .seconds(0.4)) await withErrorReporting { try await database.write { [now, syncUp, transcript] db in - _ = try Meeting(date: now, syncUpID: syncUp.id!, transcript: transcript) - .inserted(db) + try Meeting.insert( + Meeting.Draft(date: now, syncUpID: syncUp.id, transcript: transcript) + ) + .execute(db) } } } @@ -376,9 +378,9 @@ struct MeetingFooterView: View { model: RecordMeetingModel( syncUp: SyncUp(id: 1, seconds: 60, theme: .bubblegum, title: "Engineering"), attendees: [ - Attendee(name: "Blob", syncUpID: 1), - Attendee(name: "Blob Jr", syncUpID: 1), - Attendee(name: "Blob Sr", syncUpID: 1), + Attendee(id: 1, name: "Blob", syncUpID: 1), + Attendee(id: 2, name: "Blob Jr", syncUpID: 1), + Attendee(id: 3, name: "Blob Sr", syncUpID: 1), ] ) ) diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 4ec09972..d4a005f9 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -1,11 +1,11 @@ import SharingGRDB +import StructuredQueriesGRDB import SwiftUI -struct SyncUp: Codable, Hashable, FetchableRecord, MutablePersistableRecord { - static let tableName = "syncUps" - - var id: Int64? - var seconds = 60 * 5 +@Table +struct SyncUp: Codable, Hashable, Identifiable { + let id: Int + var seconds: Int = 60 * 5 var theme: Theme = .bubblegum var title = "" @@ -13,38 +13,32 @@ struct SyncUp: Codable, Hashable, FetchableRecord, MutablePersistableRecord { get { .seconds(seconds) } set { seconds = Int(newValue.components.seconds) } } +} - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID +extension SyncUp.Draft { + var duration: Duration { + get { .seconds(seconds) } + set { seconds = Int(newValue.components.seconds) } } } -struct Attendee: Codable, Hashable, FetchableRecord, MutablePersistableRecord { - static let tableName = "attendees" - - var id: Int64? +@Table +struct Attendee: Codable, Hashable, Identifiable { + let id: Int var name = "" - var syncUpID: Int64 - - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } + var syncUpID: SyncUp.ID } -struct Meeting: Codable, Hashable, FetchableRecord, MutablePersistableRecord { - static let tableName = "meetings" - - var id: Int64? +@Table +struct Meeting: Codable, Hashable, Identifiable { + let id: Int + @Column(as: Date.ISO8601Representation.self) var date: Date - var syncUpID: Int64 + var syncUpID: SyncUp.ID var transcript: String - - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } -enum Theme: String, CaseIterable, Codable, Hashable, Identifiable, DatabaseValueConvertible { +enum Theme: String, CaseIterable, Codable, Hashable, Identifiable, QueryBindable { case appIndigo case appMagenta case appOrange @@ -110,28 +104,28 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Create sync-ups table") { db in - try db.create(table: SyncUp.databaseTableName) { table in + try db.create(table: SyncUp.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("seconds", .integer).defaults(to: 5 * 60).notNull() - table.column("theme", .text).notNull().defaults(to: Theme.bubblegum) + table.column("theme", .text).notNull().defaults(to: Theme.bubblegum.rawValue) table.column("title", .text).notNull() } } migrator.registerMigration("Create attendees table") { db in - try db.create(table: Attendee.databaseTableName) { table in + try db.create(table: Attendee.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("name", .text).notNull() table.column("syncUpID", .integer) - .references(SyncUp.databaseTableName, column: "id", onDelete: .cascade) + .references(SyncUp.tableName, column: "id", onDelete: .cascade) .notNull() } } migrator.registerMigration("Create meetings table") { db in - try db.create(table: Meeting.databaseTableName) { table in + try db.create(table: Meeting.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("date", .datetime).notNull().unique().defaults(sql: "CURRENT_TIMESTAMP") table.column("syncUpID", .integer) - .references(SyncUp.databaseTableName, column: "id", onDelete: .cascade) + .references(SyncUp.tableName, column: "id", onDelete: .cascade) .notNull() table.column("transcript", .text).notNull() } @@ -150,15 +144,22 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { #if DEBUG extension Database { func insertSampleData() throws { - let design = try SyncUp(seconds: 60, theme: .appOrange, title: "Design") - .inserted(self) + let design = try SyncUp + .insert(SyncUp.Draft(seconds: 60, theme: .appOrange, title: "Design")) + .returning(\.self) + .fetchOne(self)! + for name in ["Blob", "Blob Jr", "Blob Sr", "Blob Esq", "Blob III", "Blob I"] { - _ = try Attendee(name: name, syncUpID: design.id!).inserted(self) + try Attendee + .insert(Attendee.Draft(name: name, syncUpID: design.id)) + .execute(self) } - _ = try Meeting( - date: Date().addingTimeInterval(-60 * 60 * 24 * 7), - syncUpID: design.id!, - transcript: """ + try Meeting + .insert( + Meeting.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 7), + syncUpID: design.id, + transcript: """ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \ @@ -166,19 +167,28 @@ func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ mollit anim id est laborum. """ - ) - .inserted(self) - - let engineering = try SyncUp(seconds: 60 * 10, theme: .periwinkle, title: "Engineering") - .inserted(self) + ) + ) + .execute(self) + + let engineering = try SyncUp + .insert(SyncUp.Draft(seconds: 60 * 10, theme: .periwinkle, title: "Engineering")) + .returning(\.self) + .fetchOne(self)! for name in ["Blob", "Blob Jr"] { - _ = try Attendee(name: name, syncUpID: engineering.id!).inserted(self) + try Attendee + .insert(Attendee.Draft(name: name, syncUpID: engineering.id)) + .execute(self) } - let product = try SyncUp(seconds: 60 * 30, theme: .poppy, title: "Product") - .inserted(self) + let product = try SyncUp + .insert(SyncUp.Draft(seconds: 60 * 30, theme: .poppy, title: "Product")) + .returning(\.self) + .fetchOne(self)! for name in ["Blob Sr", "Blob Jr"] { - _ = try Attendee(name: name, syncUpID: product.id!).inserted(self) + try Attendee + .insert(Attendee.Draft(name: name, syncUpID: product.id)) + .execute(self) } } } diff --git a/Examples/SyncUps/SyncUpDetail.swift b/Examples/SyncUps/SyncUpDetail.swift index bcbe013a..ec41dc69 100644 --- a/Examples/SyncUps/SyncUpDetail.swift +++ b/Examples/SyncUps/SyncUpDetail.swift @@ -42,7 +42,8 @@ final class SyncUpDetailModel: HashableObject { func deleteMeetings(atOffsets indices: IndexSet) { withErrorReporting { try database.write { db in - _ = try Meeting.deleteAll(db, keys: indices.map { details.meetings[$0].id }) + let ids = indices.map { details.meetings[$0].id } + try Meeting.where { ids.contains($0.id) }.delete().execute(db) } } } @@ -58,7 +59,7 @@ final class SyncUpDetailModel: HashableObject { try? await clock.sleep(for: .seconds(0.4)) await withErrorReporting { try await database.write { [syncUp = details.syncUp] db in - _ = try syncUp.delete(db) + try SyncUp.delete(syncUp).execute(db) } } @@ -76,7 +77,7 @@ final class SyncUpDetailModel: HashableObject { func editButtonTapped() { destination = .edit( withDependencies(from: self) { - SyncUpFormModel(syncUp: details.syncUp, attendees: details.attendees) + SyncUpFormModel(syncUp: SyncUp.Draft(details.syncUp), attendees: details.attendees) } ) } @@ -106,14 +107,20 @@ final class SyncUpDetailModel: HashableObject { let syncUp: SyncUp + struct SyncUpNotFound: Error {} + func fetch(_ db: Database) throws -> Value { - try Value( - attendees: Attendee.filter(Column("syncUpID") == syncUp.id).fetchAll(db), + guard let syncUp = try SyncUp.where({ $0.id == syncUp.id }).fetchOne(db) + else { + throw SyncUpNotFound() + } + return try Value( + attendees: Attendee.where { $0.syncUpID == syncUp.id }.fetchAll(db), meetings: Meeting - .filter(Column("syncUpID") == syncUp.id) - .order(Column("date").desc) + .where { $0.syncUpID == syncUp.id } + .order { $0.date.desc() } .fetchAll(db), - syncUp: SyncUp.fetchOne(db, key: syncUp.id) ?? SyncUp() + syncUp: syncUp ) } } @@ -289,11 +296,11 @@ struct MeetingView: View { #Preview { let _ = prepareDependencies { - $0.defaultDatabase = SyncUps.appDatabase(inMemory: true) + $0.defaultDatabase = try! SyncUps.appDatabase(inMemory: true) } @Dependency(\.defaultDatabase) var database let syncUp = try! database.read { db in - try SyncUp.fetchOne(db)! + try SyncUp.limit(1).fetchOne(db)! } NavigationStack { SyncUpDetailView( diff --git a/Examples/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUpForm.swift index e6336e55..c41bc5bd 100644 --- a/Examples/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUpForm.swift @@ -8,7 +8,7 @@ final class SyncUpFormModel: Identifiable { var attendees: [AttendeeDraft] = [] var focus: Field? var isDismissed = false - var syncUp: SyncUp + var syncUp: SyncUp.Draft @ObservationIgnored @Dependency(\.defaultDatabase) var database @ObservationIgnored @Dependency(\.uuid) var uuid @@ -24,7 +24,7 @@ final class SyncUpFormModel: Identifiable { } init( - syncUp: SyncUp, + syncUp: SyncUp.Draft, attendees: [Attendee] = [], focus: Field? = .title ) { @@ -66,10 +66,11 @@ final class SyncUpFormModel: Identifiable { } withErrorReporting { try database.write { db in - try syncUp.save(db) - try Attendee.filter(Column("syncUpID") == syncUp.id!).deleteAll(db) + let dbSyncUp = try SyncUp.upsert(syncUp).returning(\.self).fetchOne(db)! + try Attendee.where { $0.syncUpID == syncUp.id }.delete().execute(db) for attendee in attendees { - _ = try Attendee(name: attendee.name, syncUpID: syncUp.id!).inserted(db) + try Attendee.insert(Attendee.Draft(name: attendee.name, syncUpID: dbSyncUp.id)) + .execute(db) } } } @@ -160,12 +161,14 @@ extension Duration { } } -#Preview { - NavigationStack { - SyncUpFormView( - model: SyncUpFormModel( - syncUp: SyncUp() +struct SyncUpFormPreviews: PreviewProvider { + static var previews: some View { + NavigationStack { + SyncUpFormView( + model: SyncUpFormModel( + syncUp: SyncUp.Draft() + ) ) - ) + } } } diff --git a/Examples/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUpsList.swift index a66ece89..2a0f65ec 100644 --- a/Examples/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUpsList.swift @@ -1,4 +1,5 @@ import SharingGRDB +import StructuredQueries import SwiftUI import SwiftUINavigation @@ -6,7 +7,16 @@ import SwiftUINavigation @Observable final class SyncUpsListModel { var addSyncUp: SyncUpFormModel? - @ObservationIgnored @SharedReader var syncUps: [SyncUps.Record] + @ObservationIgnored @SharedReader( + .fetchAll( + SyncUp + .group(by: \.id) + .join(Attendee.all()) { $0.id.eq($1.syncUpID) } + .select { Record.Columns(attendeeCount: $1.id.count(), syncUp: $0) }, + animation: .default + ) + ) + var syncUps: [Record] @ObservationIgnored @Dependency(\.uuid) var uuid @ObservationIgnored @Dependency(\.defaultDatabase) var database @@ -14,26 +24,18 @@ final class SyncUpsListModel { addSyncUp: SyncUpFormModel? = nil ) { self.addSyncUp = addSyncUp - _syncUps = SharedReader(.fetch(SyncUps(), animation: .default)) } func addSyncUpButtonTapped() { addSyncUp = withDependencies(from: self) { - SyncUpFormModel(syncUp: SyncUp()) + SyncUpFormModel(syncUp: SyncUp.Draft()) } } - struct SyncUps: FetchKeyRequest { - struct Record: Decodable, FetchableRecord { - let syncUp: SyncUp - let attendeeCount: Int - } - func fetch(_ db: Database) throws -> [Record] { - try SyncUp.all() - .annotated(with: [SyncUp.hasMany(Attendee.self).count]) - .asRequest(of: Record.self) - .fetchAll(db) - } + @Selection + struct Record { + let attendeeCount: Int + let syncUp: SyncUp } } @@ -103,7 +105,7 @@ extension LabelStyle where Self == TrailingIconLabelStyle { #Preview { let _ = prepareDependencies { - $0.defaultDatabase = SyncUps.appDatabase(inMemory: true) + $0.defaultDatabase = try! SyncUps.appDatabase(inMemory: true) } NavigationStack { SyncUpsList(model: SyncUpsListModel()) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index e01f19bd..6edfa5e0 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "3a99a1d34c02e8fb4abb8d6b594f62456138ea71" + "revision" : "44fc8fc67470433026162474b25932afe802bf12" } }, { From 884cf42e070d1316cf6e3fed126b56d08da41dbf Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 14 Mar 2025 15:21:35 -0700 Subject: [PATCH 027/171] wip --- Examples/SyncUps/SyncUpForm.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUpForm.swift index c41bc5bd..70705122 100644 --- a/Examples/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUpForm.swift @@ -66,10 +66,10 @@ final class SyncUpFormModel: Identifiable { } withErrorReporting { try database.write { db in - let dbSyncUp = try SyncUp.upsert(syncUp).returning(\.self).fetchOne(db)! + let updatedSyncUp = try SyncUp.upsert(syncUp).returning(\.self).fetchOne(db)! try Attendee.where { $0.syncUpID == syncUp.id }.delete().execute(db) for attendee in attendees { - try Attendee.insert(Attendee.Draft(name: attendee.name, syncUpID: dbSyncUp.id)) + try Attendee.insert(Attendee.Draft(name: attendee.name, syncUpID: updatedSyncUp.id)) .execute(db) } } From a682f4a6aed99f79f30280663364bf3775177bba Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 14 Mar 2025 15:46:00 -0700 Subject: [PATCH 028/171] wip --- Examples/SyncUps/RecordMeeting.swift | 4 ++-- Examples/SyncUps/Schema.swift | 19 +++++++------------ Examples/SyncUps/SyncUpDetail.swift | 8 ++------ Examples/SyncUps/SyncUpForm.swift | 18 +++++++++--------- Examples/SyncUps/SyncUpsList.swift | 2 +- Sources/SharingGRDB/FetchKey.swift | 5 ++++- 6 files changed, 25 insertions(+), 31 deletions(-) diff --git a/Examples/SyncUps/RecordMeeting.swift b/Examples/SyncUps/RecordMeeting.swift index 4cd59314..67b3bd9b 100644 --- a/Examples/SyncUps/RecordMeeting.swift +++ b/Examples/SyncUps/RecordMeeting.swift @@ -32,11 +32,11 @@ final class RecordMeetingModel: HashableObject { } var durationPerAttendee: Duration { - syncUp.duration / attendees.count + syncUp.seconds.duration / attendees.count } var durationRemaining: Duration { - syncUp.duration - .seconds(secondsElapsed) + syncUp.seconds.duration - .seconds(secondsElapsed) } func nextButtonTapped() { diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index d4a005f9..35b11ac6 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -4,21 +4,16 @@ import SwiftUI @Table struct SyncUp: Codable, Hashable, Identifiable { - let id: Int - var seconds: Int = 60 * 5 - var theme: Theme = .bubblegum - var title = "" - - var duration: Duration { - get { .seconds(seconds) } - set { seconds = Int(newValue.components.seconds) } - } + @Column("id", primaryKey: true) let id: Int + @Column("seconds") var seconds: Int = 60 * 5 + @Column("theme") var theme: Theme = .bubblegum + @Column("title") var title = "" } -extension SyncUp.Draft { +extension Int { var duration: Duration { - get { .seconds(seconds) } - set { seconds = Int(newValue.components.seconds) } + get { .seconds(self) } + set { self = Int(newValue.components.seconds) } } } diff --git a/Examples/SyncUps/SyncUpDetail.swift b/Examples/SyncUps/SyncUpDetail.swift index ec41dc69..2b4125aa 100644 --- a/Examples/SyncUps/SyncUpDetail.swift +++ b/Examples/SyncUps/SyncUpDetail.swift @@ -107,13 +107,9 @@ final class SyncUpDetailModel: HashableObject { let syncUp: SyncUp - struct SyncUpNotFound: Error {} - func fetch(_ db: Database) throws -> Value { guard let syncUp = try SyncUp.where({ $0.id == syncUp.id }).fetchOne(db) - else { - throw SyncUpNotFound() - } + else { throw NotFound() } return try Value( attendees: Attendee.where { $0.syncUpID == syncUp.id }.fetchAll(db), meetings: Meeting @@ -143,7 +139,7 @@ struct SyncUpDetailView: View { HStack { Label("Length", systemImage: "clock") Spacer() - Text(model.details.syncUp.duration.formatted(.units())) + Text(model.details.syncUp.seconds.duration.formatted(.units())) } HStack { diff --git a/Examples/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUpForm.swift index 70705122..95097159 100644 --- a/Examples/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUpForm.swift @@ -89,11 +89,11 @@ struct SyncUpFormView: View { TextField("Title", text: $model.syncUp.title) .focused($focus, equals: .title) HStack { - Slider(value: $model.syncUp.duration.seconds, in: 5...30, step: 1) { + Slider(value: $model.syncUp.seconds.toDouble, in: 5...30, step: 1) { Text("Length") } Spacer() - Text(model.syncUp.duration.formatted(.units())) + Text(model.syncUp.seconds.duration.formatted(.units())) } ThemePicker(selection: $model.syncUp.theme) } header: { @@ -134,6 +134,13 @@ struct SyncUpFormView: View { } } +extension Int { + fileprivate var toDouble: Double { + get { Double(self) } + set { self = Int(newValue) } + } +} + struct ThemePicker: View { @Binding var selection: Theme @@ -154,13 +161,6 @@ struct ThemePicker: View { } } -extension Duration { - fileprivate var seconds: Double { - get { Double(components.seconds / 60) } - set { self = .seconds(newValue * 60) } - } -} - struct SyncUpFormPreviews: PreviewProvider { static var previews: some View { NavigationStack { diff --git a/Examples/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUpsList.swift index 2a0f65ec..8c467456 100644 --- a/Examples/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUpsList.swift @@ -80,7 +80,7 @@ struct CardView: View { HStack { Label("\(attendeeCount)", systemImage: "person.3") Spacer() - Label(syncUp.duration.formatted(.units()), systemImage: "clock") + Label(syncUp.seconds.duration.formatted(.units()), systemImage: "clock") .labelStyle(.trailingIcon) } .font(.caption) diff --git a/Sources/SharingGRDB/FetchKey.swift b/Sources/SharingGRDB/FetchKey.swift index 1c3cc2d7..709380fc 100644 --- a/Sources/SharingGRDB/FetchKey.swift +++ b/Sources/SharingGRDB/FetchKey.swift @@ -298,4 +298,7 @@ private struct FetchOne: FetchKeyRequest { } } -struct NotFound: Error {} +// TODO: Better name or somewhere else to nest? +public struct NotFound: Error { + public init() {} +} From ff272a89b74dd04e3d671acb403ce5b546b0374a Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 14 Mar 2025 16:03:01 -0700 Subject: [PATCH 029/171] wip --- Examples/SyncUps/SyncUpForm.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Examples/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUpForm.swift index 95097159..98260d21 100644 --- a/Examples/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUpForm.swift @@ -66,10 +66,13 @@ final class SyncUpFormModel: Identifiable { } withErrorReporting { try database.write { db in - let updatedSyncUp = try SyncUp.upsert(syncUp).returning(\.self).fetchOne(db)! - try Attendee.where { $0.syncUpID == syncUp.id }.delete().execute(db) + try SyncUp.upsert(syncUp).execute(db) + guard let syncUpID = syncUp.id + else { return } + + try Attendee.where { $0.syncUpID == syncUpID }.delete().execute(db) for attendee in attendees { - try Attendee.insert(Attendee.Draft(name: attendee.name, syncUpID: updatedSyncUp.id)) + try Attendee.insert(Attendee.Draft(name: attendee.name, syncUpID: syncUpID)) .execute(db) } } From 0828bae6563794801795f440a73e0243064512a0 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 16 Mar 2025 11:22:05 -0700 Subject: [PATCH 030/171] fixes --- Examples/Reminders/ReminderForm.swift | 14 +++++++----- Examples/Reminders/ReminderRow.swift | 7 ++---- Examples/Reminders/RemindersLists.swift | 4 ++-- Examples/Reminders/Schema.swift | 2 +- Examples/Reminders/SearchReminders.swift | 2 +- Examples/SyncUps/Schema.swift | 22 +++++++++---------- Examples/SyncUps/SyncUpForm.swift | 9 ++++---- Examples/SyncUps/SyncUpsList.swift | 2 +- Package.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 13 +++++------ 10 files changed, 39 insertions(+), 38 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 295a9b6d..00d24684 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -124,10 +124,10 @@ struct ReminderFormView: View { else { return } do { selectedTags = try await database.read { db in - try Tag.all() - .order(by: \.name) - .leftJoin(ReminderTag.all()) { $0.id.eq($1.tagID) } - .where { $1.reminderID.eq(reminderID) } + try Tag.order(by: \.name) + .leftJoin(ReminderTag.all()) { + $0.id.eq($1.tagID) && $1.reminderID.eq(reminderID) + } .select { tag, _ in tag } .fetchAll(db) } @@ -160,7 +160,11 @@ struct ReminderFormView: View { private func saveButtonTapped() { withErrorReporting { try database.write { db in - let reminderID = try Reminder.upsert(reminder).returning(\.id).fetchOne(db)! + guard let reminderID = try Reminder.upsert(reminder).returning(\.id).fetchOne(db) + else { + reportIssue("Could not upsert reminder") + return + } if reminder.id != nil { try ReminderTag.where { $0.reminderID == reminderID }.delete().execute(db) } diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 2c8e08f5..11cfffcc 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -56,10 +56,7 @@ struct ReminderRow: View { Button("Delete") { withErrorReporting { try database.write { db in - try Reminder - .where { $0.id == reminder.id } - .delete() - .execute(db) + try Reminder.delete(reminder).execute(db) } } } @@ -68,7 +65,7 @@ struct ReminderRow: View { withErrorReporting { try database.write { db in try Reminder - .where { $0.id == reminder.id } + .where { $0.id.eq(reminder.id) } .update { $0.isFlagged.toggle() } .execute(db) } diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 7ae5ef42..18c65317 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -126,10 +126,10 @@ struct RemindersListsView: View { completedCount: Reminder.where(\.isCompleted).count().fetchOne(db) ?? 0, flaggedCount: Reminder.where(\.isFlagged).count().fetchOne(db) ?? 0, scheduledCount: Reminder.count() - .where { #raw("date(\($0.date)) > date('now')") } + .where { #sql("date(\($0.date)) > date('now')") } .fetchOne(db) ?? 0, todayCount: Reminder.count() - .where { #raw("date(\($0.date)) = date('now')") } + .where { #sql("date(\($0.date)) = date('now')") } .fetchOne(db) ?? 0 ) } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 6d8b3fdc..7185a02e 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -32,7 +32,7 @@ struct Reminder: Codable, Equatable, Identifiable { } extension Reminder.Columns { var isPastDue: some QueryExpression { - !isCompleted && #raw("coalesce(\(date), date('now')) < date('now')") + !isCompleted && #sql("coalesce(\(date), date('now')) < date('now')") } } diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 63fe6b4c..6fa5d0f1 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -102,7 +102,7 @@ struct SearchRemindersView: View { .where(\.isCompleted) if let monthsAgo { try baseQuery - .where { #raw("\($0.date) < date('now', '-\(monthsAgo) months") } + .where { #sql("\($0.date) < date('now', '-\(raw: monthsAgo) months')") } .delete() .execute(db) } else { diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 35b11ac6..dc22558a 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -4,17 +4,10 @@ import SwiftUI @Table struct SyncUp: Codable, Hashable, Identifiable { - @Column("id", primaryKey: true) let id: Int - @Column("seconds") var seconds: Int = 60 * 5 - @Column("theme") var theme: Theme = .bubblegum - @Column("title") var title = "" -} - -extension Int { - var duration: Duration { - get { .seconds(self) } - set { self = Int(newValue.components.seconds) } - } + let id: Int + var seconds: Int = 60 * 5 + var theme: Theme = .bubblegum + var title = "" } @Table @@ -76,6 +69,13 @@ enum Theme: String, CaseIterable, Codable, Hashable, Identifiable, QueryBindable } } +extension Int { + var duration: Duration { + get { .seconds(self) } + set { self = Int(newValue.components.seconds) } + } +} + func appDatabase(inMemory: Bool = false) throws -> any DatabaseWriter { let database: any DatabaseWriter var configuration = Configuration() diff --git a/Examples/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUpForm.swift index 98260d21..d26586dd 100644 --- a/Examples/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUpForm.swift @@ -66,10 +66,11 @@ final class SyncUpFormModel: Identifiable { } withErrorReporting { try database.write { db in - try SyncUp.upsert(syncUp).execute(db) - guard let syncUpID = syncUp.id - else { return } - + guard let syncUpID = try SyncUp.upsert(syncUp).returning(\.id).fetchOne(db) + else { + reportIssue("Could not upsert sync-up.") + return + } try Attendee.where { $0.syncUpID == syncUpID }.delete().execute(db) for attendee in attendees { try Attendee.insert(Attendee.Draft(name: attendee.name, syncUpID: syncUpID)) diff --git a/Examples/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUpsList.swift index 8c467456..c08201d1 100644 --- a/Examples/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUpsList.swift @@ -11,7 +11,7 @@ final class SyncUpsListModel { .fetchAll( SyncUp .group(by: \.id) - .join(Attendee.all()) { $0.id.eq($1.syncUpID) } + .leftJoin(Attendee.all()) { $0.id.eq($1.syncUpID) } .select { Record.Columns(attendeeCount: $1.id.count(), syncUp: $0) }, animation: .default ) diff --git a/Package.swift b/Package.swift index 76c9e539..a666ef1b 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,7 @@ let package = Package( .package(url: "https://github.com/groue/GRDB.swift", from: "7.1.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), + .package(url: "https://github.com/pointfreeco/swift-structured-queries", revision: "3af90e3a25b66505a83ea1eb1738bad7f7d07e0c"), ], targets: [ .target( diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6edfa5e0..b77239e0 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "702cf47f7b8527b3bb941563266e3f886871ef870dcc100cb6cde93fcf1c910e", + "originHash" : "a4352c550be75358865b36fd5a4e06b1f554cd3e26eb510f8f7b22fa8d236011", "pins" : [ { "identity" : "combine-schedulers", @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "121a428c505c01c4ce02d5ada1c8fc3da93afce9", - "version" : "1.8.0" + "revision" : "ec2862d1364536fc22ec56a3094e7a034bbc7da8", + "version" : "1.8.1" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "21811d6230a625fa0f2e6ffa85be857075cc02c4", - "version" : "1.5.0" + "revision" : "671fa54b279fd73933b4a8b34782ebf6c8869145", + "version" : "1.5.1" } }, { @@ -132,8 +132,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "branch" : "main", - "revision" : "44fc8fc67470433026162474b25932afe802bf12" + "revision" : "3af90e3a25b66505a83ea1eb1738bad7f7d07e0c" } }, { From 7830cc4d88dc7cdc6d4c8e5d6e1b9343fe5e36c3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 16 Mar 2025 12:09:31 -0700 Subject: [PATCH 031/171] SyncUpForm tests' --- Examples/Examples.xcodeproj/project.pbxproj | 124 +++++++++++++++++- .../xcshareddata/xcschemes/SyncUps.xcscheme | 13 ++ Examples/SyncUpTests/SyncUpFormTests.swift | 59 +++++++++ Examples/SyncUps/SyncUpDetail.swift | 2 +- Examples/SyncUps/SyncUpForm.swift | 21 ++- Examples/SyncUps/SyncUpsApp.swift | 10 +- 6 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 Examples/SyncUpTests/SyncUpFormTests.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 0ca2d452..c144edd6 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA2908C82D4AF70E003F165F /* UIKitNavigation */; }; + CAD001872D874F1F00FA977A /* DependenciesTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = CAD001862D874F1F00FA977A /* DependenciesTestSupport */; }; CAE6C64D2D69017D00CE1C90 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */; }; CAF3EAB92D84D85400E7E0D0 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAF3EAB82D84D85400E7E0D0 /* StructuredQueriesGRDB */; }; CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAFDD6492D5E823A00EE099E /* SharingGRDB */; }; @@ -19,6 +20,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + CAD001812D874E6F00FA977A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CAF836902D4735620047AEB5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DCBE89CB2D483FB90071F499; + remoteInfo = SyncUps; + }; CAF836A92D4735640047AEB5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = CAF836902D4735620047AEB5 /* Project object */; @@ -30,6 +38,7 @@ /* Begin PBXFileReference section */ CA5F37542D5AFBBC002E1A9E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + CAD0017D2D874E6F00FA977A /* SyncUpTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SyncUpTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CAF836982D4735620047AEB5 /* CaseStudies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CaseStudies.app; sourceTree = BUILT_PRODUCTS_DIR; }; CAF836A82D4735640047AEB5 /* CaseStudiesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CaseStudiesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CAF836D82D4735AB0047AEB5 /* Reminders.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Reminders.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -62,6 +71,11 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + CAD0017E2D874E6F00FA977A /* SyncUpTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = SyncUpTests; + sourceTree = ""; + }; CAF8369A2D4735620047AEB5 /* CaseStudies */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -94,6 +108,14 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + CAD0017A2D874E6F00FA977A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CAD001872D874F1F00FA977A /* DependenciesTestSupport in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CAF836952D4735620047AEB5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -142,6 +164,7 @@ CAF836AB2D4735640047AEB5 /* CaseStudiesTests */, CAF836D92D4735AB0047AEB5 /* Reminders */, DCBE89CD2D483FB90071F499 /* SyncUps */, + CAD0017E2D874E6F00FA977A /* SyncUpTests */, CAF837022D4735C00047AEB5 /* Frameworks */, CAF836992D4735620047AEB5 /* Products */, ); @@ -154,6 +177,7 @@ CAF836A82D4735640047AEB5 /* CaseStudiesTests.xctest */, CAF836D82D4735AB0047AEB5 /* Reminders.app */, DCBE89CC2D483FB90071F499 /* SyncUps.app */, + CAD0017D2D874E6F00FA977A /* SyncUpTests.xctest */, ); name = Products; sourceTree = ""; @@ -168,6 +192,30 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + CAD0017C2D874E6F00FA977A /* SyncUpTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CAD001852D874E6F00FA977A /* Build configuration list for PBXNativeTarget "SyncUpTests" */; + buildPhases = ( + CAD001792D874E6F00FA977A /* Sources */, + CAD0017A2D874E6F00FA977A /* Frameworks */, + CAD0017B2D874E6F00FA977A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CAD001822D874E6F00FA977A /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + CAD0017E2D874E6F00FA977A /* SyncUpTests */, + ); + name = SyncUpTests; + packageProductDependencies = ( + CAD001862D874F1F00FA977A /* DependenciesTestSupport */, + ); + productName = SyncUpTests; + productReference = CAD0017D2D874E6F00FA977A /* SyncUpTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; CAF836972D4735620047AEB5 /* CaseStudies */ = { isa = PBXNativeTarget; buildConfigurationList = CAF836BC2D4735640047AEB5 /* Build configuration list for PBXNativeTarget "CaseStudies" */; @@ -273,9 +321,13 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1620; + LastSwiftUpdateCheck = 1630; LastUpgradeCheck = 1620; TargetAttributes = { + CAD0017C2D874E6F00FA977A = { + CreatedOnToolsVersion = 16.3; + TestTargetID = DCBE89CB2D483FB90071F499; + }; CAF836972D4735620047AEB5 = { CreatedOnToolsVersion = 16.2; }; @@ -314,11 +366,19 @@ CAF836A72D4735640047AEB5 /* CaseStudiesTests */, CAF836D72D4735AB0047AEB5 /* Reminders */, DCBE89CB2D483FB90071F499 /* SyncUps */, + CAD0017C2D874E6F00FA977A /* SyncUpTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + CAD0017B2D874E6F00FA977A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; CAF836962D4735620047AEB5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -350,6 +410,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + CAD001792D874E6F00FA977A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; CAF836942D4735620047AEB5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -381,6 +448,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + CAD001822D874E6F00FA977A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DCBE89CB2D483FB90071F499 /* SyncUps */; + targetProxy = CAD001812D874E6F00FA977A /* PBXContainerItemProxy */; + }; CAF836AA2D4735640047AEB5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CAF836972D4735620047AEB5 /* CaseStudies */; @@ -389,6 +461,42 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + CAD001832D874E6F00FA977A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SyncUpTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SyncUps.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SyncUps"; + }; + name = Debug; + }; + CAD001842D874E6F00FA977A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SyncUpTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SyncUps.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SyncUps"; + }; + name = Release; + }; CAF836BA2D4735640047AEB5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -715,6 +823,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + CAD001852D874E6F00FA977A /* Build configuration list for PBXNativeTarget "SyncUpTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CAD001832D874E6F00FA977A /* Debug */, + CAD001842D874E6F00FA977A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; CAF836932D4735620047AEB5 /* Build configuration list for PBXProject "Examples" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -795,6 +912,11 @@ package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; productName = UIKitNavigation; }; + CAD001862D874F1F00FA977A /* DependenciesTestSupport */ = { + isa = XCSwiftPackageProductDependency; + package = DC5FA7462D4C63D60082743E /* XCRemoteSwiftPackageReference "swift-dependencies" */; + productName = DependenciesTestSupport; + }; CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */ = { isa = XCSwiftPackageProductDependency; productName = StructuredQueriesGRDB; diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/SyncUps.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/SyncUps.xcscheme index 0d56a702..97c6d079 100644 --- a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/SyncUps.xcscheme +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/SyncUps.xcscheme @@ -29,6 +29,19 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + Date: Mon, 17 Mar 2025 11:20:55 -0700 Subject: [PATCH 032/171] wip --- Package.resolved | 10 +- .../StructuredQueriesGRDBCore/Database.swift | 134 ++++++++++ .../SQLiteQueryDecoder.swift | 247 ++++++++++++++++++ .../StructuredQueriesGRDBCore.swift | 216 ++++----------- 4 files changed, 439 insertions(+), 168 deletions(-) create mode 100644 Sources/StructuredQueriesGRDBCore/Database.swift create mode 100644 Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift diff --git a/Package.resolved b/Package.resolved index 2c6ea21e..a7e47c59 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "92d70bc37a2b9f0b9e3ac188bc30026638c28e5bec27e5b695175ccf8819f738", + "originHash" : "60537462d4b1a345801afdd8c6e084bd8cb6cfe4a2e79020c92c2835f6eba262", "pins" : [ { "identity" : "combine-schedulers", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "4de97e6903d29697caae1179cdef8479c64c2639", - "version" : "7.0.0" + "revision" : "6eba24d16952452a8a54f6a639491f3c8215527f", + "version" : "7.3.0" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-sharing", "state" : { - "revision" : "e4e239979e718ce52fb67ffd14ef491cede7ab43", - "version" : "2.2.0" + "revision" : "2c840cf2ae0526ad6090e7796c4e13d9a2339f4a", + "version" : "2.3.3" } }, { diff --git a/Sources/StructuredQueriesGRDBCore/Database.swift b/Sources/StructuredQueriesGRDBCore/Database.swift new file mode 100644 index 00000000..7e889f41 --- /dev/null +++ b/Sources/StructuredQueriesGRDBCore/Database.swift @@ -0,0 +1,134 @@ +import Foundation +import GRDB +import SQLite3 +import StructuredQueries + +struct Database { + private let db: GRDB.Database + private let handle: OpaquePointer + + init(_ handle: OpaquePointer, db: GRDB.Database) { + self.db = db + self.handle = handle + } + + public func execute( + _ sql: String + ) throws { + guard sqlite3_exec(handle, sql, nil, nil, nil) == SQLITE_OK + else { + throw SQLiteError(handle) + } + } + + public func execute(_ query: some StructuredQueries.Statement<()>) throws { + _ = try execute(query) as [()] + } + + public func execute( + _ query: some StructuredQueries.Statement + ) throws -> [QueryValue.QueryOutput] { + try withStatement(query) { statement in + var results: [QueryValue.QueryOutput] = [] + let decoder = SQLiteQueryDecoder(database: handle, statement: statement) + loop: while true { + let code = sqlite3_step(statement) + switch code { + case SQLITE_ROW: + try results.append(QueryValue(decoder: decoder).queryOutput) + decoder.next() + case SQLITE_DONE: + break loop + default: + throw SQLiteError(handle) + } + } + return results + } + } + + public func execute( + _ query: some StructuredQueries.Statement<(repeat each V)> + ) throws -> [(repeat (each V).QueryOutput)] { + try withStatement(query) { statement in + var results: [(repeat (each V).QueryOutput)] = [] + let decoder = SQLiteQueryDecoder(database: handle, statement: statement) + loop: while true { + let code = sqlite3_step(statement) + switch code { + case SQLITE_ROW: + try results.append((repeat (each V)(decoder: decoder).queryOutput)) + decoder.next() + case SQLITE_DONE: + break loop + default: + throw SQLiteError(handle) + } + } + return results + } + } + + public func execute( + _ query: S + ) throws -> [(S.From.QueryOutput, repeat (each J).QueryOutput)] + where S.QueryValue == (), S.Joins == (repeat each J) { + try withStatement(query) { statement in + var results: [(S.From.QueryOutput, repeat (each J).QueryOutput)] = [] + let decoder = SQLiteQueryDecoder(database: handle, statement: statement) + loop: while true { + let code = sqlite3_step(statement) + switch code { + case SQLITE_ROW: + try results.append( + ( + decoder.decodeColumns(S.From.self).queryOutput, + repeat decoder.decodeColumns((each J).self).queryOutput + ) + ) + decoder.next() + case SQLITE_DONE: + break loop + default: + throw SQLiteError(handle) + } + } + return results + } + } + + private func withStatement( + _ query: some StructuredQueries.Statement, body: (OpaquePointer) throws -> R + ) throws -> R { + let sql = query.query + let statement = try db.makeStatement(sql: sql.string) + try db.registerAccess(to: statement.databaseRegion) + for (index, binding) in zip(Int32(1)..., sql.bindings) { + let result = + switch binding { + case let .blob(blob): + sqlite3_bind_blob(statement.sqliteStatement, index, Array(blob), -1, SQLITE_TRANSIENT) + case let .double(double): + sqlite3_bind_double(statement.sqliteStatement, index, double) + case let .int(int): + sqlite3_bind_int64(statement.sqliteStatement, index, Int64(int)) + case .null: + sqlite3_bind_null(statement.sqliteStatement, index) + case let .text(text): + sqlite3_bind_text(statement.sqliteStatement, index, text, -1, SQLITE_TRANSIENT) + } + guard result == SQLITE_OK else { throw SQLiteError(handle) } + } + return try body(statement.sqliteStatement) + } +} + +private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + +struct SQLiteError: Error { + let message: String + + init(_ handle: OpaquePointer?) { + self.message = String(cString: sqlite3_errmsg(handle)) + } +} diff --git a/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift new file mode 100644 index 00000000..b520ad3e --- /dev/null +++ b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift @@ -0,0 +1,247 @@ +import SQLite3 +import StructuredQueries + +final class SQLiteQueryDecoder: QueryDecoder { + private let database: OpaquePointer? + private let statement: OpaquePointer + private var currentIndex: Int32 = 0 + + init(database: OpaquePointer?, statement: OpaquePointer) { + self.database = database + self.statement = statement + } + + @inlinable + @inline(__always) + func next() { + currentIndex = 0 + } + + @inlinable + @inline(__always) + func decode(_ type: Bool.Type) throws -> Bool { + try decode(Int.self) != 0 + } + + @inlinable + @inline(__always) + func decode(_ type: ContiguousArray.Type) throws -> ContiguousArray { + defer { currentIndex += 1 } + return ContiguousArray( + UnsafeRawBufferPointer( + start: sqlite3_column_blob(statement, currentIndex), + count: Int(sqlite3_column_bytes(statement, currentIndex)) + ) + ) + } + + @inlinable + @inline(__always) + func decode(_ type: Double.Type) throws -> Double { + defer { currentIndex += 1 } + return sqlite3_column_double(statement, currentIndex) + } + + @inlinable + @inline(__always) + func decode(_ type: Float.Type) throws -> Float { + try Float(decode(Double.self)) + } + + @inlinable + @inline(__always) + func decode(_ type: Int.Type) throws -> Int { + try Int(decode(Int64.self)) + } + + @inlinable + @inline(__always) + func decode(_ type: Int8.Type) throws -> Int8 { + try Int8(decode(Int32.self)) + } + + @inlinable + @inline(__always) + func decode(_ type: Int16.Type) throws -> Int16 { + try Int16(decode(Int32.self)) + } + + @inlinable + @inline(__always) + func decode(_ type: Int32.Type) throws -> Int32 { + defer { currentIndex += 1 } + return sqlite3_column_int(statement, currentIndex) + } + + @inlinable + @inline(__always) + func decode(_ type: Int64.Type) throws -> Int64 { + defer { currentIndex += 1 } + return sqlite3_column_int64(statement, currentIndex) + } + + @inlinable + @inline(__always) + func decode(_ type: String.Type) throws -> String { + defer { currentIndex += 1 } + return String(cString: sqlite3_column_text(statement, currentIndex)) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt.Type) throws -> UInt { + try UInt(decode(UInt64.self)) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt8.Type) throws -> UInt8 { + try UInt8(decode(Int32.self)) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt16.Type) throws -> UInt16 { + try UInt16(decode(Int32.self)) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt32.Type) throws -> UInt32 { + try UInt32(decode(Int64.self)) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt64.Type) throws -> UInt64 { + try UInt64(decode(Int64.self)) + } + + @inlinable + @inline(__always) + public func decodeColumns(_ type: T.Type = T.self) throws -> T { + try T(decoder: self) + } + + @inlinable + @inline(__always) + func decodeNil() throws -> Bool { + let isNil = sqlite3_column_type(statement, currentIndex) == SQLITE_NULL + if isNil { currentIndex += 1 } + return isNil + } + + @inlinable + @inline(__always) + func decode(_ type: Bool?.Type) throws -> Bool? { + try decode(Int?.self).map { $0 != 0 } + } + + @inlinable + @inline(__always) + func decode(_ type: ContiguousArray?.Type) throws -> ContiguousArray? { + defer { currentIndex += 1 } + guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } + return ContiguousArray( + UnsafeRawBufferPointer( + start: sqlite3_column_blob(statement, currentIndex), + count: Int(sqlite3_column_bytes(statement, currentIndex)) + ) + ) + } + + @inlinable + @inline(__always) + func decode(_ type: Float?.Type) throws -> Float? { + try decode(Double?.self).map(Float.init) + } + + @inlinable + @inline(__always) + func decode(_ type: Double?.Type) throws -> Double? { + defer { currentIndex += 1 } + guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } + return sqlite3_column_double(statement, currentIndex) + } + + @inlinable + @inline(__always) + func decode(_ type: Int?.Type) throws -> Int? { + try decode(Int64?.self).map(Int.init) + } + + @inlinable + @inline(__always) + func decode(_ type: Int8?.Type) throws -> Int8? { + try decode(Int32?.self).map(Int8.init) + } + + @inlinable + @inline(__always) + func decode(_ type: Int16?.Type) throws -> Int16? { + try decode(Int32?.self).map(Int16.init) + } + + @inlinable + @inline(__always) + func decode(_ type: Int32?.Type) throws -> Int32? { + defer { currentIndex += 1 } + guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } + return sqlite3_column_int(statement, currentIndex) + } + + @inlinable + @inline(__always) + func decode(_ type: Int64?.Type) throws -> Int64? { + defer { currentIndex += 1 } + guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } + return sqlite3_column_int64(statement, currentIndex) + } + + @inlinable + @inline(__always) + func decode(_ type: String?.Type) throws -> String? { + defer { currentIndex += 1 } + guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } + return String(cString: sqlite3_column_text(statement, currentIndex)) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt?.Type) throws -> UInt? { + try decode(UInt64?.self).map(UInt.init) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt8?.Type) throws -> UInt8? { + try decode(UInt32?.self).map(UInt8.init) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt16?.Type) throws -> UInt16? { + try decode(UInt32?.self).map(UInt16.init) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt32?.Type) throws -> UInt32? { + try decode(Int64?.self).map(UInt32.init) + } + + @inlinable + @inline(__always) + func decode(_ type: UInt64?.Type) throws -> UInt64? { + try decode(Int64?.self).map(UInt64.init) + } + + @inlinable + @inline(__always) + public func decodeColumns(_ type: T?.Type = T?.self) throws -> T? { + let index = currentIndex + let result = try T?(decoder: self) + currentIndex = index.advanced(by: T.Columns.count) + return result + } +} diff --git a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift index 314381ec..68db58a7 100644 --- a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift +++ b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift @@ -1,206 +1,96 @@ import GRDB import Foundation +import IssueReporting +import SQLite3 @_exported import StructuredQueriesCore extension StructuredQueriesCore.Statement { - // TODO: Support try Record.find(reminder.listID)? - - public func execute(_ db: Database) throws { - let query = self.query - guard !query.isEmpty else { return } - try db.execute(sql: query.string, arguments: query.arguments) + public func execute(_ db: GRDB.Database) throws + where QueryValue == () { + guard !query.isEmpty else { + reportIssue("Can't fetch from empty query") + return + } + guard let handle = db.sqliteConnection else { + reportIssue("Can't fetch from closed database connection") + return + } + try Database(handle, db: db).execute(self) } public func fetchAll( - _ db: Database + _ db: GRDB.Database ) throws -> [(repeat (each Value).QueryOutput)] where QueryValue == (repeat each Value) { - let query = self.query - guard !query.isEmpty else { return [] } - let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) - var results: [(repeat (each Value).QueryOutput)] = [] - let decoder = GRDBQueryDecoder() - while let row = try cursor.next() { - try decoder.withRow(row) { - try results.append((repeat (each Value)(decoder: decoder).queryOutput)) - } + guard !query.isEmpty else { + reportIssue("Can't fetch from empty query") + return [] } - return results + guard let handle = db.sqliteConnection else { + reportIssue("Can't fetch from closed database connection") + return [] + } + return try Database(handle, db: db).execute(self) } public func fetchAll( - _ db: Database + _ db: GRDB.Database ) throws -> [QueryValue.QueryOutput] where QueryValue: QueryRepresentable { - let query = self.query - guard !query.isEmpty else { return [] } - let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) - var results: [QueryValue.QueryOutput] = [] - let decoder = GRDBQueryDecoder() - while let row = try cursor.next() { - try decoder.withRow(row) { - try results.append(QueryValue(decoder: decoder).queryOutput) - } + guard !query.isEmpty else { + reportIssue("Can't fetch from empty query") + return [] + } + guard let handle = db.sqliteConnection else { + reportIssue("Can't fetch from closed database connection") + return [] } - return results + return try Database(handle, db: db).execute(self) } public func fetchOne( - _ db: Database + _ db: GRDB.Database ) throws -> (repeat (each Value).QueryOutput)? where QueryValue == (repeat each Value) { - let query = self.query - guard !query.isEmpty else { return nil } - let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) - guard let row = try cursor.next() else { return nil } - let decoder = GRDBQueryDecoder() - return try decoder.withRow(row) { - try (repeat (each Value)(decoder: decoder).queryOutput) + guard !query.isEmpty else { + reportIssue("Can't fetch from empty query") + return nil } + guard let handle = db.sqliteConnection else { + reportIssue("Can't fetch from closed database connection") + return nil + } + return try Database(handle, db: db).execute(self).first } public func fetchOne( - _ db: Database + _ db: GRDB.Database ) throws -> QueryValue.QueryOutput? where QueryValue: QueryRepresentable { - let query = self.query - guard !query.isEmpty else { return nil } - let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) - guard let row = try cursor.next() else { return nil } - let decoder = GRDBQueryDecoder() - return try decoder.withRow(row) { - try QueryValue(decoder: decoder).queryOutput + guard !query.isEmpty else { + reportIssue("Can't fetch from empty query") + return nil + } + guard let handle = db.sqliteConnection else { + reportIssue("Can't fetch from closed database connection") + return nil } + return try Database(handle, db: db).execute(self).first } } extension SelectStatement where QueryValue == () { public func fetchAll( - _ db: Database + _ db: GRDB.Database ) throws -> [(From.QueryOutput, repeat (each J).QueryOutput)] where Joins == (repeat each J) { - let query = self.query - guard !query.isEmpty else { return [] } - let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) - var results: [(From.QueryOutput, repeat (each J).QueryOutput)] = [] - let decoder = GRDBQueryDecoder() - while let row = try cursor.next() { - try decoder.withRow(row) { - try results.append( - ( - From(decoder: decoder).queryOutput, - repeat (each J)(decoder: decoder).queryOutput - ) - ) - } - } - return results + try self.selectStar().fetchAll(db) } public func fetchOne( - _ db: Database + _ db: GRDB.Database ) throws -> (From.QueryOutput, repeat (each J).QueryOutput)? where Joins == (repeat each J) { - let query = self.query - guard !query.isEmpty else { return nil } - let cursor = try Row.fetchCursor(db, sql: query.string, arguments: query.arguments) - guard let row = try cursor.next() else { return nil } - let decoder = GRDBQueryDecoder() - return try decoder.withRow(row) { - try ( - From(decoder: decoder).queryOutput, - repeat (each J)(decoder: decoder).queryOutput - ) - } - } -} - -fileprivate final class GRDBQueryDecoder: QueryDecoder { - private var statement: OpaquePointer? - private var currentIndex: Int = 0 - private var currentRow: Row! - - public init() {} - - func withRow(_ row: Row, body: () throws -> R) rethrows -> R { - currentRow = row - defer { - currentIndex = 0 - currentRow = nil - } - return try body() - } - - func decodeNil() throws -> Bool { - guard currentIndex < currentRow.count else { throw DecodingError() } - let isNil = currentRow.hasNull(atIndex: currentIndex) - if isNil { currentIndex += 1 } - return isNil - } - - func decode(_ type: Double.Type) throws -> Double { - defer { currentIndex += 1 } - guard - currentIndex < currentRow.count, - let value = currentRow[currentIndex] as? Double - else { throw DecodingError() } - return value - } - - func decode(_ type: Int64.Type) throws -> Int64 { - defer { currentIndex += 1 } - guard - currentIndex < currentRow.count, - let value = currentRow[currentIndex] as? Int64 - else { throw DecodingError() } - return value - } - - func decode(_ type: String.Type) throws -> String { - defer { currentIndex += 1 } - guard - currentIndex < currentRow.count, - let value = currentRow[currentIndex] as? String - else { throw DecodingError() } - return value - } - - func decode(_ type: [UInt8].Type) throws -> [UInt8] { - defer { currentIndex += 1 } - guard - currentIndex < currentRow.count, - let value = currentRow[currentIndex] as? Data - else { throw DecodingError() } - return [UInt8](value) - } - - // TODO: Better error handling/messaging - private struct DecodingError: Error { - init() { - print("!!!") - } - } -} - -fileprivate extension QueryFragment { - var arguments: StatementArguments { - StatementArguments(bindings.map(\.databaseValue)) - } -} - -fileprivate extension QueryBinding /* : DatabaseValueConvertible */ { - var databaseValue: DatabaseValue { - switch self { - case .blob(let blob): - return Data(blob).databaseValue - case .double(let double): - return double.databaseValue - case .int(let int): - return int.databaseValue - case .null: - return .null - case .text(let text): - return text.databaseValue - } + try self.selectStar().fetchOne(db) } } From 2baffe5748c99bc1538c4a65f4e45cf2d84ad084 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 17 Mar 2025 12:17:41 -0700 Subject: [PATCH 033/171] wip --- Examples/Reminders/RemindersListDetail.swift | 1 + Sources/StructuredQueriesGRDBCore/Database.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 1434478b..338f9f46 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -17,6 +17,7 @@ struct RemindersListDetailView: View { self.remindersList = remindersList _ordering = AppStorage(wrappedValue: .dueDate, "ordering_list_\(remindersList.id)") _showCompleted = AppStorage(wrappedValue: false, "show_completed_list_\(remindersList.id)") + _reminderStates = SharedReader(wrappedValue: [], remindersKey) } var body: some View { diff --git a/Sources/StructuredQueriesGRDBCore/Database.swift b/Sources/StructuredQueriesGRDBCore/Database.swift index 7e889f41..2c80e02a 100644 --- a/Sources/StructuredQueriesGRDBCore/Database.swift +++ b/Sources/StructuredQueriesGRDBCore/Database.swift @@ -103,6 +103,7 @@ struct Database { let sql = query.query let statement = try db.makeStatement(sql: sql.string) try db.registerAccess(to: statement.databaseRegion) + try db.notifyChanges(in: statement.databaseRegion) for (index, binding) in zip(Int32(1)..., sql.bindings) { let result = switch binding { From 30b87a1c83d6c647454b818f0f868ec0888529fa Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 17 Mar 2025 12:19:16 -0700 Subject: [PATCH 034/171] wip --- Sources/StructuredQueriesGRDBCore/Database.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/Database.swift b/Sources/StructuredQueriesGRDBCore/Database.swift index 2c80e02a..11fbb2cf 100644 --- a/Sources/StructuredQueriesGRDBCore/Database.swift +++ b/Sources/StructuredQueriesGRDBCore/Database.swift @@ -103,7 +103,6 @@ struct Database { let sql = query.query let statement = try db.makeStatement(sql: sql.string) try db.registerAccess(to: statement.databaseRegion) - try db.notifyChanges(in: statement.databaseRegion) for (index, binding) in zip(Int32(1)..., sql.bindings) { let result = switch binding { @@ -120,7 +119,9 @@ struct Database { } guard result == SQLITE_OK else { throw SQLiteError(handle) } } - return try body(statement.sqliteStatement) + let results = try body(statement.sqliteStatement) + try db.notifyChanges(in: statement.databaseRegion) + return results } } From c44bae318ac9e0affb4d4c7b856997eb84bc40a6 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 17 Mar 2025 12:52:54 -0700 Subject: [PATCH 035/171] wip --- .../StructuredQueries/StatementKey.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 681057ae..22061015 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -132,6 +132,23 @@ extension SharedReaderKey { #if canImport(SwiftUI) @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { + public static func fetchAll( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) -> Self + where + S.QueryValue == (), + S.Joins == (repeat each J), + Self == FetchKey<[(S.From.QueryOutput, repeat (each J).QueryOutput)]>.Default + { + fetch( + FetchAllStatementRequest(statement: statement.selectStar()), + database: database, + animation: animation + ) + } + public static func fetchAll( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, From 3c1a7031be77ab80e0a1d0dc4da38892df9a82ad Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 17 Mar 2025 13:22:51 -0700 Subject: [PATCH 036/171] wip --- Package.swift | 2 ++ SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 76c9e539..fa6cf78f 100644 --- a/Package.swift +++ b/Package.swift @@ -27,6 +27,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/groue/GRDB.swift", from: "7.1.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), ], @@ -51,6 +52,7 @@ let package = Package( name: "StructuredQueriesGRDBCore", dependencies: [ .product(name: "GRDB", package: "GRDB.swift"), + .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), .product(name: "StructuredQueriesCore", package: "swift-structured-queries"), ] ), diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 66598936..27745be0 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "702cf47f7b8527b3bb941563266e3f886871ef870dcc100cb6cde93fcf1c910e", + "originHash" : "266d7b1ad94045e74884d79be79bb6458c5690d9ec3623f56a03aa7291fffd9a", "pins" : [ { "identity" : "combine-schedulers", From f985140fa50204538b7c6af8c5ca565ad6af4c56 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 17 Mar 2025 14:05:45 -0700 Subject: [PATCH 037/171] wip --- Sources/SharingGRDB/StructuredQueries/StatementKey.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 22061015..6141c013 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -9,6 +9,7 @@ import StructuredQueriesGRDBCore import SwiftUI #endif +// TODO: Is it possible to loosen this availability? @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { public static func fetchAll( From fc646afac43f63e046ef9c8c04749402835c7280 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 17 Mar 2025 14:34:45 -0700 Subject: [PATCH 038/171] wip --- Examples/Reminders/RemindersLists.swift | 2 +- Examples/Reminders/TagsForm.swift | 6 +++--- Examples/SyncUps/SyncUpsList.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 80ba1c08..871ce189 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -10,7 +10,7 @@ struct RemindersListsView: View { .fetchAll( RemindersList.group(by: \.id) .join(Reminder.incomplete) { $0.id.eq($1.remindersListID) } - .select { ReminderListState.Columns(reminderCount: $1.id.count(), remindersList: $0) }, + .select { ReminderListState.Columns(reminderCount: $1.count(), remindersList: $0) }, animation: .default ) ) diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index 0499706c..092c7278 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -49,14 +49,14 @@ struct TagsView: View { .group(by: \.id) .join(ReminderTag.all()) { $0.id.eq($1.tagID)} .join(Reminder.all()) { $1.reminderID.eq($2.id)} - .having { $2.id.count().gt(0) } - .order { ($2.id.count().desc(), $0.name) } + .having { $2.count().gt(0) } + .order { ($2.count().desc(), $0.name) } .limit(3) .select { tags, _, _ in tags } .fetchAll(db) let rest = try Tag - .where { !$0.id.in(top.compactMap(\.id)) } + .where { !$0.id.in(top.map(\.id)) } .fetchAll(db) return Value(rest: rest, top: top) diff --git a/Examples/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUpsList.swift index 5475238f..eef7cb64 100644 --- a/Examples/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUpsList.swift @@ -12,7 +12,7 @@ final class SyncUpsListModel { SyncUp .group(by: \.id) .leftJoin(Attendee.all()) { $0.id.eq($1.syncUpID) } - .select { Record.Columns(attendeeCount: $1.id.count(), syncUp: $0) }, + .select { Record.Columns(attendeeCount: $1.count(), syncUp: $0) }, animation: .default ) ) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 27745be0..f380c95f 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "32069b68292addf77811570c2b58125064231f4c" + "revision" : "c28bca833a40cb64e7b2a5a25f168c2db2975078" } }, { From 0321e2f843cfa70060334f2598696581d5bfb823 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Mar 2025 18:42:21 -0700 Subject: [PATCH 039/171] fixes --- Package.resolved | 141 ++++++++++++++++++ .../StructuredQueries/StatementKey.swift | 2 +- .../StructuredQueriesGRDBCore/Database.swift | 12 +- .../SQLiteQueryDecoder.swift | 2 +- 4 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..e883c062 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,141 @@ +{ + "originHash" : "fc38420b0b11af3b962084b31efba190930cecfe714adce4b5d86c7d260c44c0", + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "5928286acce13def418ec36d05a001a9641086f2", + "version" : "1.0.3" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift", + "state" : { + "revision" : "6eba24d16952452a8a54f6a639491f3c8215527f", + "version" : "7.3.0" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "ec2862d1364536fc22ec56a3094e7a034bbc7da8", + "version" : "1.8.1" + } + }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", + "version" : "1.4.3" + } + }, + { + "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", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "671fa54b279fd73933b4a8b34782ebf6c8869145", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-sharing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-sharing", + "state" : { + "revision" : "2c840cf2ae0526ad6090e7796c4e13d9a2339f4a", + "version" : "2.3.3" + } + }, + { + "identity" : "swift-structured-queries", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-structured-queries", + "state" : { + "branch" : "main", + "revision" : "0f39769674ed9a0b569f16e348a546cb7bbcf05e" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" + } + } + ], + "version" : 3 +} diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 6141c013..a995a764 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -23,7 +23,7 @@ extension SharedReaderKey { { fetch( FetchAllStatementRequest(statement: statement.selectStar()), - database: database, + database: database ) } diff --git a/Sources/StructuredQueriesGRDBCore/Database.swift b/Sources/StructuredQueriesGRDBCore/Database.swift index 11fbb2cf..e999c9f8 100644 --- a/Sources/StructuredQueriesGRDBCore/Database.swift +++ b/Sources/StructuredQueriesGRDBCore/Database.swift @@ -1,7 +1,7 @@ import Foundation import GRDB import SQLite3 -import StructuredQueries +import StructuredQueriesCore struct Database { private let db: GRDB.Database @@ -21,12 +21,12 @@ struct Database { } } - public func execute(_ query: some StructuredQueries.Statement<()>) throws { + public func execute(_ query: some StructuredQueriesCore.Statement<()>) throws { _ = try execute(query) as [()] } public func execute( - _ query: some StructuredQueries.Statement + _ query: some StructuredQueriesCore.Statement ) throws -> [QueryValue.QueryOutput] { try withStatement(query) { statement in var results: [QueryValue.QueryOutput] = [] @@ -48,7 +48,7 @@ struct Database { } public func execute( - _ query: some StructuredQueries.Statement<(repeat each V)> + _ query: some StructuredQueriesCore.Statement<(repeat each V)> ) throws -> [(repeat (each V).QueryOutput)] { try withStatement(query) { statement in var results: [(repeat (each V).QueryOutput)] = [] @@ -69,7 +69,7 @@ struct Database { } } - public func execute( + public func execute( _ query: S ) throws -> [(S.From.QueryOutput, repeat (each J).QueryOutput)] where S.QueryValue == (), S.Joins == (repeat each J) { @@ -98,7 +98,7 @@ struct Database { } private func withStatement( - _ query: some StructuredQueries.Statement, body: (OpaquePointer) throws -> R + _ query: some StructuredQueriesCore.Statement, body: (OpaquePointer) throws -> R ) throws -> R { let sql = query.query let statement = try db.makeStatement(sql: sql.string) diff --git a/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift index b520ad3e..4e21b60e 100644 --- a/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift +++ b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift @@ -1,5 +1,5 @@ import SQLite3 -import StructuredQueries +import StructuredQueriesCore final class SQLiteQueryDecoder: QueryDecoder { private let database: OpaquePointer? From fbfe8cb39e2e80df1c1268b782d22c9b69179827 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 18 Mar 2025 18:45:14 -0700 Subject: [PATCH 040/171] wip --- Sources/StructuredQueriesGRDBCore/Database.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/StructuredQueriesGRDBCore/Database.swift b/Sources/StructuredQueriesGRDBCore/Database.swift index e999c9f8..38b0f812 100644 --- a/Sources/StructuredQueriesGRDBCore/Database.swift +++ b/Sources/StructuredQueriesGRDBCore/Database.swift @@ -107,7 +107,9 @@ struct Database { let result = switch binding { case let .blob(blob): - sqlite3_bind_blob(statement.sqliteStatement, index, Array(blob), -1, SQLITE_TRANSIENT) + sqlite3_bind_blob( + statement.sqliteStatement, index, Array(blob), Int32(blob.count), SQLITE_TRANSIENT + ) case let .double(double): sqlite3_bind_double(statement.sqliteStatement, index, double) case let .int(int): From 9fcd36b29a90d360c799308ce410d983ceb018f4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 19 Mar 2025 12:32:39 -0700 Subject: [PATCH 041/171] fix --- Examples/Reminders/ReminderForm.swift | 4 +--- Examples/Reminders/RemindersListDetail.swift | 4 ++-- Examples/Reminders/Schema.swift | 2 +- Package.resolved | 11 +---------- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../SQLiteQueryDecoder.swift | 2 +- 6 files changed, 7 insertions(+), 18 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index d31e4b62..ba42b54a 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -135,9 +135,7 @@ struct ReminderFormView: View { do { selectedTags = try await database.read { db in try Tag.order(by: \.name) - .leftJoin(ReminderTag.all()) { - $0.id.eq($1.tagID) && $1.reminderID.eq(reminderID) - } + .join(ReminderTag.all()) { $0.id.eq($1.tagID) && $1.reminderID.eq(reminderID) } .select { tag, _ in tag } .fetchAll(db) } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 338f9f46..7c418f11 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -145,8 +145,8 @@ struct RemindersListDetailView: View { extension Reminder { static let withTags = group(by: \.id) - .leftJoin(ReminderTag.all()) { $0.id.eq($1.reminderID) } - .leftJoin(Tag.all()) { $1.tagID.eq($2.id) } + .join(ReminderTag.all()) { $0.id.eq($1.reminderID) } + .join(Tag.all()) { $1.tagID.eq($2.id) } } struct RemindersListDetailPreview: PreviewProvider { diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 21fe64f7..8ad90e0d 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -30,7 +30,7 @@ struct Reminder: Codable, Equatable, Identifiable { } static let incomplete = Self.where { !$0.isCompleted } } -extension Reminder.Columns { +extension Reminder.TableColumns { var isPastDue: some QueryExpression { !isCompleted && #sql("coalesce(\(date), date('now')) < date('now')") } diff --git a/Package.resolved b/Package.resolved index e883c062..3a20f47f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "fc38420b0b11af3b962084b31efba190930cecfe714adce4b5d86c7d260c44c0", + "originHash" : "c33e1f57b8a5fc20bfaec1c9a156c28901b2b3351b9aa7a3ec82114782ad65fb", "pins" : [ { "identity" : "combine-schedulers", @@ -109,15 +109,6 @@ "version" : "2.3.3" } }, - { - "identity" : "swift-structured-queries", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-structured-queries", - "state" : { - "branch" : "main", - "revision" : "0f39769674ed9a0b569f16e348a546cb7bbcf05e" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index f380c95f..2a68db2f 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "c28bca833a40cb64e7b2a5a25f168c2db2975078" + "revision" : "5a36956ed41a07df0ff84230ad23d036b2968ce0" } }, { diff --git a/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift index 4e21b60e..c9594c8b 100644 --- a/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift +++ b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift @@ -241,7 +241,7 @@ final class SQLiteQueryDecoder: QueryDecoder { public func decodeColumns(_ type: T?.Type = T?.self) throws -> T? { let index = currentIndex let result = try T?(decoder: self) - currentIndex = index.advanced(by: T.Columns.count) + currentIndex = index.advanced(by: T.TableColumns.count) return result } } From 1a744301b5ad7aacb78c2ea31856cdb66f87953d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 19 Mar 2025 17:07:07 -0700 Subject: [PATCH 042/171] wip --- Examples/Reminders/RemindersListDetail.swift | 6 +++--- Examples/Reminders/RemindersLists.swift | 13 +++++++++---- .../xcshareddata/swiftpm/Package.resolved | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 7c418f11..4dd0fc1d 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -132,7 +132,7 @@ struct RemindersListDetailView: View { } @Selection - fileprivate struct ReminderState: Decodable, Identifiable { + fileprivate struct ReminderState: Identifiable { var id: Reminder.ID { reminder.id } var reminder: Reminder var isPastDue: Bool @@ -145,8 +145,8 @@ struct RemindersListDetailView: View { extension Reminder { static let withTags = group(by: \.id) - .join(ReminderTag.all()) { $0.id.eq($1.reminderID) } - .join(Tag.all()) { $1.tagID.eq($2.id) } + .leftJoin(ReminderTag.all()) { $0.id.eq($1.reminderID) } + .leftJoin(Tag.all()) { $1.tagID.eq($2.id) } } struct RemindersListDetailPreview: PreviewProvider { diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 871ce189..8922a497 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -6,11 +6,16 @@ import StructuredQueries import SwiftUI struct RemindersListsView: View { - @State.SharedReader( + @SharedReader( .fetchAll( RemindersList.group(by: \.id) - .join(Reminder.incomplete) { $0.id.eq($1.remindersListID) } - .select { ReminderListState.Columns(reminderCount: $1.count(), remindersList: $0) }, + .leftJoin(Reminder.incomplete) { $0.id.eq($1.remindersListID) } + .select { + ReminderListState.Columns( + reminderCount: #sql("total(NOT \($1.isCompleted))"), + remindersList: $0 + ) + }, animation: .default ) ) @@ -114,7 +119,7 @@ struct RemindersListsView: View { } @Selection - fileprivate struct ReminderListState: Decodable, Identifiable { + fileprivate struct ReminderListState: Identifiable { var id: RemindersList.ID { remindersList.id } var reminderCount: Int var remindersList: RemindersList diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2a68db2f..31fcf15f 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "5a36956ed41a07df0ff84230ad23d036b2968ce0" + "revision" : "e3133c89190d75497611265e7452239014fa8692" } }, { From 0447953b28d0d5f0b5cd22c52213b23fdd342209 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 19 Mar 2025 17:11:32 -0700 Subject: [PATCH 043/171] wip --- Examples/Reminders/RemindersLists.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 8922a497..026095ac 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -9,7 +9,7 @@ struct RemindersListsView: View { @SharedReader( .fetchAll( RemindersList.group(by: \.id) - .leftJoin(Reminder.incomplete) { $0.id.eq($1.remindersListID) } + .leftJoin(Reminder.all()) { $0.id.eq($1.remindersListID) } .select { ReminderListState.Columns( reminderCount: #sql("total(NOT \($1.isCompleted))"), From eb4970bc8a5bc483529a3a7bc2378fe2ebb0209b Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 20 Mar 2025 08:45:35 -0500 Subject: [PATCH 044/171] wip --- Examples/Reminders/ReminderForm.swift | 6 +++++- Examples/Reminders/RemindersLists.swift | 17 +++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index ba42b54a..30ee5eaf 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -174,8 +174,12 @@ struct ReminderFormView: View { return } if reminder.id != nil { - try ReminderTag.where { $0.reminderID == reminderID }.delete().execute(db) + try ReminderTag.where { $0.reminderID == reminderID } + .delete() + .execute(db) } + guard !selectedTags.isEmpty + else { return } try ReminderTag.insert( selectedTags.map { tag in ReminderTag(reminderID: reminderID, tagID: tag.id) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 026095ac..90082b9b 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -8,14 +8,15 @@ import SwiftUI struct RemindersListsView: View { @SharedReader( .fetchAll( - RemindersList.group(by: \.id) - .leftJoin(Reminder.all()) { $0.id.eq($1.remindersListID) } - .select { - ReminderListState.Columns( - reminderCount: #sql("total(NOT \($1.isCompleted))"), - remindersList: $0 - ) - }, + RemindersList + .group(by: \.id) + .leftJoin(Reminder.all()) { $0.id.eq($1.remindersListID) } + .select { + ReminderListState.Columns( + reminderCount: #sql("total(NOT \($1.isCompleted))"), + remindersList: $0 + ) + }, animation: .default ) ) From 6a6c1ff5363ce90c44a7e5b642c9d4310f0f82e5 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 20 Mar 2025 08:47:53 -0500 Subject: [PATCH 045/171] wip --- Examples/Reminders/RemindersLists.swift | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 90082b9b..95d24440 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -8,15 +8,15 @@ import SwiftUI struct RemindersListsView: View { @SharedReader( .fetchAll( - RemindersList - .group(by: \.id) - .leftJoin(Reminder.all()) { $0.id.eq($1.remindersListID) } - .select { - ReminderListState.Columns( - reminderCount: #sql("total(NOT \($1.isCompleted))"), - remindersList: $0 - ) - }, + RemindersList + .group(by: \.id) + .leftJoin(Reminder.all()) { $0.id.eq($1.remindersListID) } + .select { + ReminderListState.Columns( + reminderCount: #sql("total(NOT \($1.isCompleted))"), + remindersList: $0 + ) + }, animation: .default ) ) @@ -72,7 +72,7 @@ struct RemindersListsView: View { } } .buttonStyle(.plain) - + Section { ForEach(remindersLists) { state in NavigationLink { From 06cea85cc4a63ca012df7133e96c9ad1d163fe53 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 20 Mar 2025 12:39:49 -0700 Subject: [PATCH 046/171] wip --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../StructuredQueriesGRDBCore/Database.swift | 29 ++++++++++++++----- .../StructuredQueriesGRDBCore.swift | 20 ------------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 31fcf15f..4520ea28 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "e3133c89190d75497611265e7452239014fa8692" + "revision" : "d552b276893d290c63685c476265c0b0fb258733" } }, { diff --git a/Sources/StructuredQueriesGRDBCore/Database.swift b/Sources/StructuredQueriesGRDBCore/Database.swift index 38b0f812..9072720e 100644 --- a/Sources/StructuredQueriesGRDBCore/Database.swift +++ b/Sources/StructuredQueriesGRDBCore/Database.swift @@ -1,5 +1,6 @@ import Foundation import GRDB +import IssueReporting import SQLite3 import StructuredQueriesCore @@ -28,7 +29,12 @@ struct Database { public func execute( _ query: some StructuredQueriesCore.Statement ) throws -> [QueryValue.QueryOutput] { - try withStatement(query) { statement in + let query = query.query + guard !query.isEmpty else { + reportIssue("Can't fetch from empty query") + return [] + } + return try withStatement(query) { statement in var results: [QueryValue.QueryOutput] = [] let decoder = SQLiteQueryDecoder(database: handle, statement: statement) loop: while true { @@ -50,7 +56,12 @@ struct Database { public func execute( _ query: some StructuredQueriesCore.Statement<(repeat each V)> ) throws -> [(repeat (each V).QueryOutput)] { - try withStatement(query) { statement in + let query = query.query + guard !query.isEmpty else { + reportIssue("Can't fetch from empty query") + return [] + } + return try withStatement(query) { statement in var results: [(repeat (each V).QueryOutput)] = [] let decoder = SQLiteQueryDecoder(database: handle, statement: statement) loop: while true { @@ -73,7 +84,12 @@ struct Database { _ query: S ) throws -> [(S.From.QueryOutput, repeat (each J).QueryOutput)] where S.QueryValue == (), S.Joins == (repeat each J) { - try withStatement(query) { statement in + let query = query.query + guard !query.isEmpty else { + reportIssue("Can't fetch from empty query") + return [] + } + return try withStatement(query) { statement in var results: [(S.From.QueryOutput, repeat (each J).QueryOutput)] = [] let decoder = SQLiteQueryDecoder(database: handle, statement: statement) loop: while true { @@ -98,12 +114,11 @@ struct Database { } private func withStatement( - _ query: some StructuredQueriesCore.Statement, body: (OpaquePointer) throws -> R + _ query: QueryFragment, body: (OpaquePointer) throws -> R ) throws -> R { - let sql = query.query - let statement = try db.makeStatement(sql: sql.string) + let statement = try db.makeStatement(sql: query.string) try db.registerAccess(to: statement.databaseRegion) - for (index, binding) in zip(Int32(1)..., sql.bindings) { + for (index, binding) in zip(Int32(1)..., query.bindings) { let result = switch binding { case let .blob(blob): diff --git a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift index 68db58a7..2ce36c62 100644 --- a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift +++ b/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift @@ -7,10 +7,6 @@ import SQLite3 extension StructuredQueriesCore.Statement { public func execute(_ db: GRDB.Database) throws where QueryValue == () { - guard !query.isEmpty else { - reportIssue("Can't fetch from empty query") - return - } guard let handle = db.sqliteConnection else { reportIssue("Can't fetch from closed database connection") return @@ -22,10 +18,6 @@ extension StructuredQueriesCore.Statement { _ db: GRDB.Database ) throws -> [(repeat (each Value).QueryOutput)] where QueryValue == (repeat each Value) { - guard !query.isEmpty else { - reportIssue("Can't fetch from empty query") - return [] - } guard let handle = db.sqliteConnection else { reportIssue("Can't fetch from closed database connection") return [] @@ -37,10 +29,6 @@ extension StructuredQueriesCore.Statement { _ db: GRDB.Database ) throws -> [QueryValue.QueryOutput] where QueryValue: QueryRepresentable { - guard !query.isEmpty else { - reportIssue("Can't fetch from empty query") - return [] - } guard let handle = db.sqliteConnection else { reportIssue("Can't fetch from closed database connection") return [] @@ -52,10 +40,6 @@ extension StructuredQueriesCore.Statement { _ db: GRDB.Database ) throws -> (repeat (each Value).QueryOutput)? where QueryValue == (repeat each Value) { - guard !query.isEmpty else { - reportIssue("Can't fetch from empty query") - return nil - } guard let handle = db.sqliteConnection else { reportIssue("Can't fetch from closed database connection") return nil @@ -67,10 +51,6 @@ extension StructuredQueriesCore.Statement { _ db: GRDB.Database ) throws -> QueryValue.QueryOutput? where QueryValue: QueryRepresentable { - guard !query.isEmpty else { - reportIssue("Can't fetch from empty query") - return nil - } guard let handle = db.sqliteConnection else { reportIssue("Can't fetch from closed database connection") return nil From 39c3015b08c888b04a6d72abe72c90ccd6cbd926 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Mar 2025 14:21:21 -0500 Subject: [PATCH 047/171] wip --- Examples/SyncUps/Schema.swift | 49 +++++++++++-------- .../xcshareddata/swiftpm/Package.resolved | 2 +- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index c287e8f0..6972abd3 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -100,21 +100,30 @@ func appDatabase() throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Create sync-ups table") { db in - try db.create(table: SyncUp.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("seconds", .integer).defaults(to: 5 * 60).notNull() - table.column("theme", .text).notNull().defaults(to: Theme.bubblegum.rawValue) - table.column("title", .text).notNull() - } + // TODO: possible to use "\(SyncUp.id)" without table qualification? + try #sql( + """ + CREATE TABLE \(SyncUp.self) ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "seconds" INTEGER NOT NULL DEFAULT 300, + "theme" TEXT NOT NULL DEFAULT '\(raw: Theme.bubblegum.rawValue)', + "title" TEXT NOT NULL + ) + """ + ) + .execute(db) } migrator.registerMigration("Create attendees table") { db in - try db.create(table: Attendee.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("name", .text).notNull() - table.column("syncUpID", .integer) - .references(SyncUp.tableName, column: "id", onDelete: .cascade) - .notNull() - } + try #sql( + """ + CREATE TABLE \(Attendee.self) ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + FOREIGN KEY("syncUpID") NOT NULL REFERENCES \(SyncUp.self)("id") + ) + """ + ) + .execute(db) } migrator.registerMigration("Create meetings table") { db in try db.create(table: Meeting.tableName) { table in @@ -156,13 +165,13 @@ func appDatabase() throws -> any DatabaseWriter { date: Date().addingTimeInterval(-60 * 60 * 24 * 7), syncUpID: design.id, transcript: """ - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ - exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \ - dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \ - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ - mollit anim id est laborum. - """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ + exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \ + dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \ + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ + mollit anim id est laborum. + """ ) ) .execute(self) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4520ea28..7844ad53 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "d552b276893d290c63685c476265c0b0fb258733" + "revision" : "98a8f92fc40711de909171e1107b053e77003c7d" } }, { From c8e47b8fc177e3f49b51e52f7fb88871fbfa475b Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Mar 2025 20:02:54 -0500 Subject: [PATCH 048/171] wip; --- Examples/SyncUps/Schema.swift | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 6972abd3..003cea8e 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -119,21 +119,26 @@ func appDatabase() throws -> any DatabaseWriter { CREATE TABLE \(Attendee.self) ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL, - FOREIGN KEY("syncUpID") NOT NULL REFERENCES \(SyncUp.self)("id") + "syncUpID" INTEGER NOT NULL, + FOREIGN KEY("syncUpID") REFERENCES \(SyncUp.self)("id") ON DELETE CASCADE ) """ ) .execute(db) } migrator.registerMigration("Create meetings table") { db in - try db.create(table: Meeting.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("date", .datetime).notNull().unique().defaults(sql: "CURRENT_TIMESTAMP") - table.column("syncUpID", .integer) - .references(SyncUp.tableName, column: "id", onDelete: .cascade) - .notNull() - table.column("transcript", .text).notNull() - } + try #sql( + """ + CREATE TABLE \(Meeting.self) ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "date" TEXT NOT NULL UNIQUE DEFAULT CURRENT_TIMESTAMP, + "syncUpID" INTEGER NOT NULL, + "transcript" TEXT NOT NULL, + FOREIGN KEY("syncUpID") REFERENCES \(SyncUp.self)("id") ON DELETE CASCADE + ) + """ + ) + .execute(db) } #if DEBUG migrator.registerMigration("Insert sample data") { db in From 8a4277e068ffc8ea16baa9ee1e75121ba1b28a65 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 21 Mar 2025 20:15:09 -0500 Subject: [PATCH 049/171] wip --- Examples/Reminders/Schema.swift | 96 ++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 8ad90e0d..6ee7b7e3 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -72,40 +72,76 @@ func appDatabase() throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Add reminders lists table") { db in - try db.create(table: RemindersList.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("color", .integer).defaults(to: 0x4a99ef).notNull() - table.column("name", .text).notNull() - } + try #sql( + """ + CREATE TABLE "remindersLists" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "color" INTEGER NOT NULL DEFAULT '\(raw: 0x4a99ef)', + "name" TEXT NOT NULL + ) + """ + ) + .execute(db) } migrator.registerMigration("Add reminders table") { db in - try db.create(table: Reminder.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("date", .date) - table.column("isCompleted", .boolean).defaults(to: false).notNull() - table.column("isFlagged", .boolean).defaults(to: false).notNull() - table.column("remindersListID", .integer) - .references(RemindersList.tableName, column: "id", onDelete: .cascade) - .notNull() - table.column("notes", .text).notNull() - table.column("priority", .integer) - table.column("title", .text).notNull() - } - try db.create(indexOn: Reminder.tableName, columns: [Reminder.columns.remindersListID.name]) + try #sql( + """ + CREATE TABLE "reminders" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "date" TEXT, + "isCompleted" INTEGER NOT NULL DEFAULT 0, + "isFlagged" INTEGER NOT NULL DEFAULT 0, + "remindersListID" INTEGER NOT NULL, + "notes" TEXT NOT NULL, + "priority" INTEGER, + "title" TEXT NOT NULL, + + FOREIGN KEY("remindersListID") REFERENCES "remindersLists"("id") ON DELETE CASCADE + ) + """ + ) + .execute(db) + try #sql( + """ + CREATE INDEX "reminders_remindersListID" ON "reminders"("remindersListID") + """ + ) + .execute(db) } migrator.registerMigration("Add tags table") { db in - try db.create(table: Tag.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("name", .text).notNull().collate(.nocase).unique() - } - try db.create(table: ReminderTag.tableName) { table in - table.column("reminderID", .integer).notNull() - .references(Reminder.tableName, column: "id", onDelete: .cascade) - table.column("tagID", .integer).notNull() - .references(Tag.tableName, column: "id", onDelete: .cascade) - } - try db.create(indexOn: ReminderTag.tableName, columns: [ReminderTag.columns.reminderID.name]) - try db.create(indexOn: ReminderTag.tableName, columns: [ReminderTag.columns.tagID.name]) + try #sql( + """ + CREATE TABLE "tags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL COLLATE NOCASE UNIQUE + ) + """ + ) + .execute(db) + try #sql( + """ + CREATE TABLE "remindersTags" ( + "reminderID" INTEGER NOT NULL, + "tagID" INTEGER NOT NULL, + + FOREIGN KEY("reminderID") REFERENCES "reminders"("id") ON DELETE CASCADE, + FOREIGN KEY("tagID") REFERENCES "tags"("id") ON DELETE CASCADE + ) + """ + ) + .execute(db) + try #sql( + """ + CREATE INDEX "remindersTags_reminderID" ON "remindersTags"("reminderID") + """ + ) + .execute(db) + try #sql( + """ + CREATE INDEX "remindersTags_tagID" ON "remindersTags"("tagID") + """ + ) + .execute(db) } #if DEBUG migrator.registerMigration("Add mock data") { db in From dff45ba4a83aa8d03e8897b2e045ef54a656e518 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 10:12:08 -0700 Subject: [PATCH 050/171] wip --- Examples/Reminders/ReminderRow.swift | 25 +++++++++++---------- Examples/Reminders/Schema.swift | 26 +++++++++++++--------- Examples/SyncUpTests/SyncUpFormTests.swift | 4 ++-- Examples/SyncUps/Schema.swift | 8 +++---- 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 417ab33b..83247bea 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -11,12 +11,12 @@ struct ReminderRow: View { @State var editReminder: Reminder? @Dependency(\.defaultDatabase) private var database - + var body: some View { HStack { HStack(alignment: .top) { Button(action: completeButtonTapped) { - Image(systemName: reminder.isCompleted ? "circle.inset.filled": "circle") + Image(systemName: reminder.isCompleted ? "circle.inset.filled" : "circle") .foregroundStyle(.gray) .font(.title2) .padding([.trailing], 5) @@ -105,23 +105,24 @@ struct ReminderRow: View { private var subtitleText: Text { let tagsText = tags.reduce(Text(reminder.date == nil ? "" : " ")) { result, tag in - result + Text("#\(tag) ") + result + + Text("#\(tag) ") .foregroundStyle(.gray) .bold() } return (dueText + tagsText).font(.callout) } - + private func title(for reminder: Reminder) -> some View { - let exclamations = String(repeating: "!", count: reminder.priority ?? 0) - + (reminder.priority == nil ? "" : " ") - return ( - Text(exclamations) - .foregroundStyle(reminder.isCompleted ? .gray : Color.hex(remindersList.color)) + let exclamations = + String(repeating: "!", count: reminder.priority?.rawValue ?? 0) + + (reminder.priority == nil ? "" : " ") + return + (Text(exclamations) + .foregroundStyle(reminder.isCompleted ? .gray : Color.hex(remindersList.color)) + Text(reminder.title) - .foregroundStyle(reminder.isCompleted ? .gray : .primary) - ) - .font(.title3) + .foregroundStyle(reminder.isCompleted ? .gray : .primary)) + .font(.title3) } } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 6ee7b7e3..41af700a 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -5,21 +5,21 @@ import SharingGRDB import StructuredQueriesGRDB @Table -struct RemindersList: Codable, Hashable, Identifiable { +struct RemindersList: Hashable, Identifiable { var id: Int64 var color = 0x4a99ef var name = "" } @Table -struct Reminder: Codable, Equatable, Identifiable { +struct Reminder: Equatable, Identifiable { var id: Int64 @Column(as: Date.ISO8601Representation?.self) var date: Date? var isCompleted = false var isFlagged = false var notes = "" - var priority: Int? + var priority: Priority? var remindersListID: Int64 var title = "" static func searching(_ text: String) -> Where { @@ -36,14 +36,20 @@ extension Reminder.TableColumns { } } +enum Priority: Int, QueryBindable { + case low = 1 + case medium + case high +} + @Table -struct Tag: Codable { +struct Tag { var id: Int64 var name = "" } @Table("remindersTags") -struct ReminderTag: Codable { +struct ReminderTag { var reminderID: Int64 var tagID: Int64 } @@ -189,7 +195,7 @@ func appDatabase() throws -> any DatabaseWriter { Reminder.Draft( date: Date(), notes: "Ask about diet", - priority: 3, + priority: .high, remindersListID: 1, title: "Doctor appointment" ), @@ -207,21 +213,21 @@ func appDatabase() throws -> any DatabaseWriter { Reminder.Draft( date: Date().addingTimeInterval(60 * 60 * 24 * 2), isFlagged: true, - priority: 3, + priority: .high, remindersListID: 2, title: "Pick up kids from school" ), Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, - priority: 1, + priority: .low, remindersListID: 2, title: "Get laundry" ), Reminder.Draft( date: Date().addingTimeInterval(60 * 60 * 24 * 4), isCompleted: false, - priority: 3, + priority: .high, remindersListID: 2, title: "Take out trash" ), @@ -238,7 +244,7 @@ func appDatabase() throws -> any DatabaseWriter { Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, - priority: 2, + priority: .medium, remindersListID: 3, title: "Send weekly emails" ), diff --git a/Examples/SyncUpTests/SyncUpFormTests.swift b/Examples/SyncUpTests/SyncUpFormTests.swift index 87655c1f..fe55c3a2 100644 --- a/Examples/SyncUpTests/SyncUpFormTests.swift +++ b/Examples/SyncUpTests/SyncUpFormTests.swift @@ -10,7 +10,7 @@ struct SyncUpFormTests { @Test func saveNew() async throws { prepareDependencies { - $0.defaultDatabase = try! SyncUps.appDatabase(inMemory: true) + $0.defaultDatabase = try! SyncUps.appDatabase() $0.uuid = .incrementing } let draft = SyncUp.Draft(title: "Morning Sync") @@ -33,7 +33,7 @@ struct SyncUpFormTests { @Test func updateExisting() async throws { prepareDependencies { - $0.defaultDatabase = try! SyncUps.appDatabase(inMemory: true) + $0.defaultDatabase = try! SyncUps.appDatabase() $0.uuid = .incrementing } let existingSyncUp = try await database.read { db in diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 003cea8e..6e54ad54 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -172,10 +172,10 @@ func appDatabase() throws -> any DatabaseWriter { transcript: """ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ - exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \ - dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \ - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ - mollit anim id est laborum. + exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute \ + irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla \ + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia \ + deserunt mollit anim id est laborum. """ ) ) From 84a274f3ff2963f8782770bbdcf5de50784fc1ed Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 11:24:14 -0700 Subject: [PATCH 051/171] wip --- .../StructuredQueriesGRDBCore/Database.swift | 14 +- .../SQLiteQueryDecoder.swift | 201 +----------------- 2 files changed, 18 insertions(+), 197 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/Database.swift b/Sources/StructuredQueriesGRDBCore/Database.swift index 9072720e..4be3e501 100644 --- a/Sources/StructuredQueriesGRDBCore/Database.swift +++ b/Sources/StructuredQueriesGRDBCore/Database.swift @@ -36,12 +36,12 @@ struct Database { } return try withStatement(query) { statement in var results: [QueryValue.QueryOutput] = [] - let decoder = SQLiteQueryDecoder(database: handle, statement: statement) + var decoder = SQLiteQueryDecoder(database: handle, statement: statement) loop: while true { let code = sqlite3_step(statement) switch code { case SQLITE_ROW: - try results.append(QueryValue(decoder: decoder).queryOutput) + try results.append(decoder.decodeColumns(QueryValue.self)) decoder.next() case SQLITE_DONE: break loop @@ -63,12 +63,12 @@ struct Database { } return try withStatement(query) { statement in var results: [(repeat (each V).QueryOutput)] = [] - let decoder = SQLiteQueryDecoder(database: handle, statement: statement) + var decoder = SQLiteQueryDecoder(database: handle, statement: statement) loop: while true { let code = sqlite3_step(statement) switch code { case SQLITE_ROW: - try results.append((repeat (each V)(decoder: decoder).queryOutput)) + try results.append(decoder.decodeColumns((repeat each V).self)) decoder.next() case SQLITE_DONE: break loop @@ -91,15 +91,15 @@ struct Database { } return try withStatement(query) { statement in var results: [(S.From.QueryOutput, repeat (each J).QueryOutput)] = [] - let decoder = SQLiteQueryDecoder(database: handle, statement: statement) + var decoder = SQLiteQueryDecoder(database: handle, statement: statement) loop: while true { let code = sqlite3_step(statement) switch code { case SQLITE_ROW: try results.append( ( - decoder.decodeColumns(S.From.self).queryOutput, - repeat decoder.decodeColumns((each J).self).queryOutput + decoder.decodeColumns(S.From.self), + repeat decoder.decodeColumns((each J).self) ) ) decoder.next() diff --git a/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift index c9594c8b..eb8ab2ee 100644 --- a/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift +++ b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift @@ -1,7 +1,7 @@ import SQLite3 import StructuredQueriesCore -final class SQLiteQueryDecoder: QueryDecoder { +struct SQLiteQueryDecoder: QueryDecoder { private let database: OpaquePointer? private let statement: OpaquePointer private var currentIndex: Int32 = 0 @@ -13,136 +13,16 @@ final class SQLiteQueryDecoder: QueryDecoder { @inlinable @inline(__always) - func next() { + mutating func next() { currentIndex = 0 } @inlinable @inline(__always) - func decode(_ type: Bool.Type) throws -> Bool { - try decode(Int.self) != 0 - } - - @inlinable - @inline(__always) - func decode(_ type: ContiguousArray.Type) throws -> ContiguousArray { - defer { currentIndex += 1 } - return ContiguousArray( - UnsafeRawBufferPointer( - start: sqlite3_column_blob(statement, currentIndex), - count: Int(sqlite3_column_bytes(statement, currentIndex)) - ) - ) - } - - @inlinable - @inline(__always) - func decode(_ type: Double.Type) throws -> Double { - defer { currentIndex += 1 } - return sqlite3_column_double(statement, currentIndex) - } - - @inlinable - @inline(__always) - func decode(_ type: Float.Type) throws -> Float { - try Float(decode(Double.self)) - } - - @inlinable - @inline(__always) - func decode(_ type: Int.Type) throws -> Int { - try Int(decode(Int64.self)) - } - - @inlinable - @inline(__always) - func decode(_ type: Int8.Type) throws -> Int8 { - try Int8(decode(Int32.self)) - } - - @inlinable - @inline(__always) - func decode(_ type: Int16.Type) throws -> Int16 { - try Int16(decode(Int32.self)) - } - - @inlinable - @inline(__always) - func decode(_ type: Int32.Type) throws -> Int32 { - defer { currentIndex += 1 } - return sqlite3_column_int(statement, currentIndex) - } - - @inlinable - @inline(__always) - func decode(_ type: Int64.Type) throws -> Int64 { - defer { currentIndex += 1 } - return sqlite3_column_int64(statement, currentIndex) - } - - @inlinable - @inline(__always) - func decode(_ type: String.Type) throws -> String { - defer { currentIndex += 1 } - return String(cString: sqlite3_column_text(statement, currentIndex)) - } - - @inlinable - @inline(__always) - func decode(_ type: UInt.Type) throws -> UInt { - try UInt(decode(UInt64.self)) - } - - @inlinable - @inline(__always) - func decode(_ type: UInt8.Type) throws -> UInt8 { - try UInt8(decode(Int32.self)) - } - - @inlinable - @inline(__always) - func decode(_ type: UInt16.Type) throws -> UInt16 { - try UInt16(decode(Int32.self)) - } - - @inlinable - @inline(__always) - func decode(_ type: UInt32.Type) throws -> UInt32 { - try UInt32(decode(Int64.self)) - } - - @inlinable - @inline(__always) - func decode(_ type: UInt64.Type) throws -> UInt64 { - try UInt64(decode(Int64.self)) - } - - @inlinable - @inline(__always) - public func decodeColumns(_ type: T.Type = T.self) throws -> T { - try T(decoder: self) - } - - @inlinable - @inline(__always) - func decodeNil() throws -> Bool { - let isNil = sqlite3_column_type(statement, currentIndex) == SQLITE_NULL - if isNil { currentIndex += 1 } - return isNil - } - - @inlinable - @inline(__always) - func decode(_ type: Bool?.Type) throws -> Bool? { - try decode(Int?.self).map { $0 != 0 } - } - - @inlinable - @inline(__always) - func decode(_ type: ContiguousArray?.Type) throws -> ContiguousArray? { + mutating func decode(_ columnType: [UInt8].Type) throws -> [UInt8]? { defer { currentIndex += 1 } guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } - return ContiguousArray( + return [UInt8]( UnsafeRawBufferPointer( start: sqlite3_column_blob(statement, currentIndex), count: Int(sqlite3_column_bytes(statement, currentIndex)) @@ -152,13 +32,7 @@ final class SQLiteQueryDecoder: QueryDecoder { @inlinable @inline(__always) - func decode(_ type: Float?.Type) throws -> Float? { - try decode(Double?.self).map(Float.init) - } - - @inlinable - @inline(__always) - func decode(_ type: Double?.Type) throws -> Double? { + mutating func decode(_ columnType: Double.Type) throws -> Double? { defer { currentIndex += 1 } guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } return sqlite3_column_double(statement, currentIndex) @@ -166,33 +40,7 @@ final class SQLiteQueryDecoder: QueryDecoder { @inlinable @inline(__always) - func decode(_ type: Int?.Type) throws -> Int? { - try decode(Int64?.self).map(Int.init) - } - - @inlinable - @inline(__always) - func decode(_ type: Int8?.Type) throws -> Int8? { - try decode(Int32?.self).map(Int8.init) - } - - @inlinable - @inline(__always) - func decode(_ type: Int16?.Type) throws -> Int16? { - try decode(Int32?.self).map(Int16.init) - } - - @inlinable - @inline(__always) - func decode(_ type: Int32?.Type) throws -> Int32? { - defer { currentIndex += 1 } - guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } - return sqlite3_column_int(statement, currentIndex) - } - - @inlinable - @inline(__always) - func decode(_ type: Int64?.Type) throws -> Int64? { + mutating func decode(_ columnType: Int64.Type) throws -> Int64? { defer { currentIndex += 1 } guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } return sqlite3_column_int64(statement, currentIndex) @@ -200,7 +48,7 @@ final class SQLiteQueryDecoder: QueryDecoder { @inlinable @inline(__always) - func decode(_ type: String?.Type) throws -> String? { + mutating func decode(_ columnType: String.Type) throws -> String? { defer { currentIndex += 1 } guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } return String(cString: sqlite3_column_text(statement, currentIndex)) @@ -208,40 +56,13 @@ final class SQLiteQueryDecoder: QueryDecoder { @inlinable @inline(__always) - func decode(_ type: UInt?.Type) throws -> UInt? { - try decode(UInt64?.self).map(UInt.init) - } - - @inlinable - @inline(__always) - func decode(_ type: UInt8?.Type) throws -> UInt8? { - try decode(UInt32?.self).map(UInt8.init) - } - - @inlinable - @inline(__always) - func decode(_ type: UInt16?.Type) throws -> UInt16? { - try decode(UInt32?.self).map(UInt16.init) - } - - @inlinable - @inline(__always) - func decode(_ type: UInt32?.Type) throws -> UInt32? { - try decode(Int64?.self).map(UInt32.init) - } - - @inlinable - @inline(__always) - func decode(_ type: UInt64?.Type) throws -> UInt64? { - try decode(Int64?.self).map(UInt64.init) + mutating func decode(_ columnType: Bool.Type) throws -> Bool? { + try decode(Int64.self).map { $0 != 0 } } @inlinable @inline(__always) - public func decodeColumns(_ type: T?.Type = T?.self) throws -> T? { - let index = currentIndex - let result = try T?(decoder: self) - currentIndex = index.advanced(by: T.TableColumns.count) - return result + mutating func decode(_ columnType: Int.Type) throws -> Int? { + try decode(Int64.self).map(Int.init) } } From 98495b124589da727ddeec22a8af204c74f3ca9a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 12:44:18 -0700 Subject: [PATCH 052/171] wip --- .../StructuredQueriesGRDBCore/Database.swift | 153 ------------------ ...iesGRDBCore.swift => Statement+GRDB.swift} | 46 +++--- 2 files changed, 21 insertions(+), 178 deletions(-) delete mode 100644 Sources/StructuredQueriesGRDBCore/Database.swift rename Sources/StructuredQueriesGRDBCore/{StructuredQueriesGRDBCore.swift => Statement+GRDB.swift} (58%) diff --git a/Sources/StructuredQueriesGRDBCore/Database.swift b/Sources/StructuredQueriesGRDBCore/Database.swift deleted file mode 100644 index 4be3e501..00000000 --- a/Sources/StructuredQueriesGRDBCore/Database.swift +++ /dev/null @@ -1,153 +0,0 @@ -import Foundation -import GRDB -import IssueReporting -import SQLite3 -import StructuredQueriesCore - -struct Database { - private let db: GRDB.Database - private let handle: OpaquePointer - - init(_ handle: OpaquePointer, db: GRDB.Database) { - self.db = db - self.handle = handle - } - - public func execute( - _ sql: String - ) throws { - guard sqlite3_exec(handle, sql, nil, nil, nil) == SQLITE_OK - else { - throw SQLiteError(handle) - } - } - - public func execute(_ query: some StructuredQueriesCore.Statement<()>) throws { - _ = try execute(query) as [()] - } - - public func execute( - _ query: some StructuredQueriesCore.Statement - ) throws -> [QueryValue.QueryOutput] { - let query = query.query - guard !query.isEmpty else { - reportIssue("Can't fetch from empty query") - return [] - } - return try withStatement(query) { statement in - var results: [QueryValue.QueryOutput] = [] - var decoder = SQLiteQueryDecoder(database: handle, statement: statement) - loop: while true { - let code = sqlite3_step(statement) - switch code { - case SQLITE_ROW: - try results.append(decoder.decodeColumns(QueryValue.self)) - decoder.next() - case SQLITE_DONE: - break loop - default: - throw SQLiteError(handle) - } - } - return results - } - } - - public func execute( - _ query: some StructuredQueriesCore.Statement<(repeat each V)> - ) throws -> [(repeat (each V).QueryOutput)] { - let query = query.query - guard !query.isEmpty else { - reportIssue("Can't fetch from empty query") - return [] - } - return try withStatement(query) { statement in - var results: [(repeat (each V).QueryOutput)] = [] - var decoder = SQLiteQueryDecoder(database: handle, statement: statement) - loop: while true { - let code = sqlite3_step(statement) - switch code { - case SQLITE_ROW: - try results.append(decoder.decodeColumns((repeat each V).self)) - decoder.next() - case SQLITE_DONE: - break loop - default: - throw SQLiteError(handle) - } - } - return results - } - } - - public func execute( - _ query: S - ) throws -> [(S.From.QueryOutput, repeat (each J).QueryOutput)] - where S.QueryValue == (), S.Joins == (repeat each J) { - let query = query.query - guard !query.isEmpty else { - reportIssue("Can't fetch from empty query") - return [] - } - return try withStatement(query) { statement in - var results: [(S.From.QueryOutput, repeat (each J).QueryOutput)] = [] - var decoder = SQLiteQueryDecoder(database: handle, statement: statement) - loop: while true { - let code = sqlite3_step(statement) - switch code { - case SQLITE_ROW: - try results.append( - ( - decoder.decodeColumns(S.From.self), - repeat decoder.decodeColumns((each J).self) - ) - ) - decoder.next() - case SQLITE_DONE: - break loop - default: - throw SQLiteError(handle) - } - } - return results - } - } - - private func withStatement( - _ query: QueryFragment, body: (OpaquePointer) throws -> R - ) throws -> R { - let statement = try db.makeStatement(sql: query.string) - try db.registerAccess(to: statement.databaseRegion) - for (index, binding) in zip(Int32(1)..., query.bindings) { - let result = - switch binding { - case let .blob(blob): - sqlite3_bind_blob( - statement.sqliteStatement, index, Array(blob), Int32(blob.count), SQLITE_TRANSIENT - ) - case let .double(double): - sqlite3_bind_double(statement.sqliteStatement, index, double) - case let .int(int): - sqlite3_bind_int64(statement.sqliteStatement, index, Int64(int)) - case .null: - sqlite3_bind_null(statement.sqliteStatement, index) - case let .text(text): - sqlite3_bind_text(statement.sqliteStatement, index, text, -1, SQLITE_TRANSIENT) - } - guard result == SQLITE_OK else { throw SQLiteError(handle) } - } - let results = try body(statement.sqliteStatement) - try db.notifyChanges(in: statement.databaseRegion) - return results - } -} - -private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) - -struct SQLiteError: Error { - let message: String - - init(_ handle: OpaquePointer?) { - self.message = String(cString: sqlite3_errmsg(handle)) - } -} diff --git a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift similarity index 58% rename from Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift rename to Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index 2ce36c62..b512f979 100644 --- a/Sources/StructuredQueriesGRDBCore/StructuredQueriesGRDBCore.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -4,61 +4,57 @@ import IssueReporting import SQLite3 @_exported import StructuredQueriesCore +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension StructuredQueriesCore.Statement { public func execute(_ db: GRDB.Database) throws where QueryValue == () { - guard let handle = db.sqliteConnection else { - reportIssue("Can't fetch from closed database connection") - return - } - try Database(handle, db: db).execute(self) + try fetchCursor(db).next() } public func fetchAll( _ db: GRDB.Database ) throws -> [(repeat (each Value).QueryOutput)] where QueryValue == (repeat each Value) { - guard let handle = db.sqliteConnection else { - reportIssue("Can't fetch from closed database connection") - return [] - } - return try Database(handle, db: db).execute(self) + let cursor = try fetchCursor(db) + return try Array(cursor) } public func fetchAll( _ db: GRDB.Database ) throws -> [QueryValue.QueryOutput] where QueryValue: QueryRepresentable { - guard let handle = db.sqliteConnection else { - reportIssue("Can't fetch from closed database connection") - return [] - } - return try Database(handle, db: db).execute(self) + try Array(fetchCursor(db)) } public func fetchOne( _ db: GRDB.Database ) throws -> (repeat (each Value).QueryOutput)? where QueryValue == (repeat each Value) { - guard let handle = db.sqliteConnection else { - reportIssue("Can't fetch from closed database connection") - return nil - } - return try Database(handle, db: db).execute(self).first + let cursor = try fetchCursor(db) + return try cursor.next() } public func fetchOne( _ db: GRDB.Database ) throws -> QueryValue.QueryOutput? where QueryValue: QueryRepresentable { - guard let handle = db.sqliteConnection else { - reportIssue("Can't fetch from closed database connection") - return nil - } - return try Database(handle, db: db).execute(self).first + try fetchCursor(db).next() + } + + public func fetchCursor( + _ db: GRDB.Database + ) throws -> QueryCursor + where QueryValue == (repeat each Value) { + try QueryCursor(db: db, query: self) + } + + public func fetchCursor(_ db: GRDB.Database) throws -> QueryCursor + where QueryValue: QueryRepresentable { + try QueryCursor(db: db, query: self) } } +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SelectStatement where QueryValue == () { public func fetchAll( _ db: GRDB.Database From c18de063fc77e42abce9f744340b93774cdec8b4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 12:52:36 -0700 Subject: [PATCH 053/171] wip --- .../Internal/Exports.swift | 1 + .../QueryCursor.swift | 62 +++++++++++++++++++ .../Statement+GRDB.swift | 31 ++++------ 3 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 Sources/StructuredQueriesGRDBCore/Internal/Exports.swift create mode 100644 Sources/StructuredQueriesGRDBCore/QueryCursor.swift diff --git a/Sources/StructuredQueriesGRDBCore/Internal/Exports.swift b/Sources/StructuredQueriesGRDBCore/Internal/Exports.swift new file mode 100644 index 00000000..320bdbac --- /dev/null +++ b/Sources/StructuredQueriesGRDBCore/Internal/Exports.swift @@ -0,0 +1 @@ +@_exported import StructuredQueriesCore diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift new file mode 100644 index 00000000..6c24a339 --- /dev/null +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -0,0 +1,62 @@ +import GRDB +import SQLite3 +import StructuredQueriesCore + +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) +public final class QueryCursor: DatabaseCursor { + public typealias Element = (repeat (each QueryValue).QueryOutput) + + public var _isDone = false + public let _statement: GRDB.Statement + + private var decoder: SQLiteQueryDecoder + + init( + db: GRDB.Database, + query: some StructuredQueriesCore.Statement<(repeat each QueryValue)> + ) throws { + let query = query.query + guard !query.isEmpty else { throw EmptyQuery() } + _statement = try db.makeStatement(sql: query.string) + decoder = SQLiteQueryDecoder( + database: db.sqliteConnection, + statement: _statement.sqliteStatement + ) + for (index, binding) in zip(Int32(1)..., query.bindings) { + let result = + switch binding { + case let .blob(blob): + sqlite3_bind_blob( + _statement.sqliteStatement, index, Array(blob), Int32(blob.count), SQLITE_TRANSIENT + ) + case let .double(double): + sqlite3_bind_double(_statement.sqliteStatement, index, double) + case let .int(int): + sqlite3_bind_int64(_statement.sqliteStatement, index, Int64(int)) + case .null: + sqlite3_bind_null(_statement.sqliteStatement, index) + case let .text(text): + sqlite3_bind_text(_statement.sqliteStatement, index, text, -1, SQLITE_TRANSIENT) + } + guard result == SQLITE_OK else { throw SQLiteError(db.sqliteConnection) } + } + } + + public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { + let element = try decoder.decodeColumns((repeat each QueryValue).self) + decoder.next() + return element + } + + private struct EmptyQuery: Error {} +} + +private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + +struct SQLiteError: Error { + let message: String + + init(_ handle: OpaquePointer?) { + self.message = String(cString: sqlite3_errmsg(handle)) + } +} diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index b512f979..729aff2d 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -1,54 +1,47 @@ import GRDB -import Foundation -import IssueReporting import SQLite3 -@_exported import StructuredQueriesCore +import StructuredQueriesCore @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension StructuredQueriesCore.Statement { - public func execute(_ db: GRDB.Database) throws - where QueryValue == () { + public func execute(_ db: Database) throws where QueryValue == () { try fetchCursor(db).next() } public func fetchAll( - _ db: GRDB.Database + _ db: Database ) throws -> [(repeat (each Value).QueryOutput)] where QueryValue == (repeat each Value) { let cursor = try fetchCursor(db) return try Array(cursor) } - public func fetchAll( - _ db: GRDB.Database - ) throws -> [QueryValue.QueryOutput] + public func fetchAll(_ db: Database) throws -> [QueryValue.QueryOutput] where QueryValue: QueryRepresentable { try Array(fetchCursor(db)) } public func fetchOne( - _ db: GRDB.Database + _ db: Database ) throws -> (repeat (each Value).QueryOutput)? where QueryValue == (repeat each Value) { let cursor = try fetchCursor(db) return try cursor.next() } - public func fetchOne( - _ db: GRDB.Database - ) throws -> QueryValue.QueryOutput? + public func fetchOne(_ db: Database) throws -> QueryValue.QueryOutput? where QueryValue: QueryRepresentable { try fetchCursor(db).next() } public func fetchCursor( - _ db: GRDB.Database + _ db: Database ) throws -> QueryCursor where QueryValue == (repeat each Value) { try QueryCursor(db: db, query: self) } - public func fetchCursor(_ db: GRDB.Database) throws -> QueryCursor + public func fetchCursor(_ db: Database) throws -> QueryCursor where QueryValue: QueryRepresentable { try QueryCursor(db: db, query: self) } @@ -57,16 +50,16 @@ extension StructuredQueriesCore.Statement { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SelectStatement where QueryValue == () { public func fetchAll( - _ db: GRDB.Database + _ db: Database ) throws -> [(From.QueryOutput, repeat (each J).QueryOutput)] where Joins == (repeat each J) { - try self.selectStar().fetchAll(db) + try selectStar().fetchAll(db) } public func fetchOne( - _ db: GRDB.Database + _ db: Database ) throws -> (From.QueryOutput, repeat (each J).QueryOutput)? where Joins == (repeat each J) { - try self.selectStar().fetchOne(db) + try selectStar().fetchOne(db) } } From d411fc5415370007bfa8d4e86369d2d5ce1d6304 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 13:22:12 -0700 Subject: [PATCH 054/171] wip --- .../QueryCursor.swift | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index 6c24a339..136f098a 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -1,3 +1,4 @@ +import Foundation import GRDB import SQLite3 import StructuredQueriesCore @@ -18,28 +19,15 @@ public final class QueryCursor: DatabaseCur let query = query.query guard !query.isEmpty else { throw EmptyQuery() } _statement = try db.makeStatement(sql: query.string) + _statement.arguments = StatementArguments(query.bindings.map(\.databaseValue)) decoder = SQLiteQueryDecoder( database: db.sqliteConnection, statement: _statement.sqliteStatement ) - for (index, binding) in zip(Int32(1)..., query.bindings) { - let result = - switch binding { - case let .blob(blob): - sqlite3_bind_blob( - _statement.sqliteStatement, index, Array(blob), Int32(blob.count), SQLITE_TRANSIENT - ) - case let .double(double): - sqlite3_bind_double(_statement.sqliteStatement, index, double) - case let .int(int): - sqlite3_bind_int64(_statement.sqliteStatement, index, Int64(int)) - case .null: - sqlite3_bind_null(_statement.sqliteStatement, index) - case let .text(text): - sqlite3_bind_text(_statement.sqliteStatement, index, text, -1, SQLITE_TRANSIENT) - } - guard result == SQLITE_OK else { throw SQLiteError(db.sqliteConnection) } - } + } + + deinit { + sqlite3_reset(_statement.sqliteStatement) } public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { @@ -53,10 +41,19 @@ public final class QueryCursor: DatabaseCur private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) -struct SQLiteError: Error { - let message: String - - init(_ handle: OpaquePointer?) { - self.message = String(cString: sqlite3_errmsg(handle)) +extension QueryBinding { + fileprivate var databaseValue: DatabaseValue { + switch self { + case let .blob(blob): + return Data(blob).databaseValue + case let .double(double): + return double.databaseValue + case let .int(int): + return int.databaseValue + case .null: + return .null + case let .text(text): + return text.databaseValue + } } } From 7375c72388d855d2b5a8bd952c5f3b42101cb4ed Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 13:28:13 -0700 Subject: [PATCH 055/171] wip --- Sources/StructuredQueriesGRDBCore/QueryCursor.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index 136f098a..78009eee 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -39,8 +39,6 @@ public final class QueryCursor: DatabaseCur private struct EmptyQuery: Error {} } -private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) - extension QueryBinding { fileprivate var databaseValue: DatabaseValue { switch self { From 706441cfda4ee319421270ea63b0e0f1c59cb03b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 14:12:12 -0700 Subject: [PATCH 056/171] wip --- .../StructuredQueries/StatementKey.swift | 221 ++++++++++++------ .../QueryCursor.swift | 67 ++++-- .../Statement+GRDB.swift | 46 ++-- 3 files changed, 232 insertions(+), 102 deletions(-) diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index a995a764..3b88625b 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -9,7 +9,37 @@ import StructuredQueriesGRDBCore import SwiftUI #endif -// TODO: Is it possible to loosen this availability? +extension SharedReaderKey { + public static func fetchAll( + _ statement: S, + database: (any DatabaseReader)? = nil + ) -> Self + where + S.QueryValue == (), + S.Joins == (), + Self == FetchKey<[S.From.QueryOutput]>.Default + { + let statement = statement.selectStar() + return fetchAll(statement, database: database) + } + + public static func fetchAll( + _ statement: S, + database: (any DatabaseReader)? = nil + ) -> Self + where S.QueryValue: QueryRepresentable, Self == FetchKey<[S.QueryValue.QueryOutput]>.Default { + fetch(FetchAllStatementValueRequest(statement: statement), database: database) + } + + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil + ) -> Self + where Self == FetchKey { + fetch(FetchOneStatementValueRequest(statement: statement), database: database) + } +} + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { public static func fetchAll( @@ -21,10 +51,7 @@ extension SharedReaderKey { S.Joins == (repeat each J), Self == FetchKey<[(S.From.QueryOutput, repeat (each J).QueryOutput)]>.Default { - fetch( - FetchAllStatementRequest(statement: statement.selectStar()), - database: database - ) + fetchAll(statement.selectStar(), database: database) } public static func fetchAll< @@ -39,31 +66,51 @@ extension SharedReaderKey { S.QueryValue == (V1, repeat each V2), Self == FetchKey<[(V1.QueryOutput, repeat (each V2).QueryOutput)]>.Default { - fetch(FetchAllStatementRequest(statement: statement), database: database) + fetch(FetchAllStatementPackRequest(statement: statement), database: database) + } + + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, + database: (any DatabaseReader)? = nil + ) -> Self + where Self == FetchKey<(repeat (each Value).QueryOutput)> { + fetch(FetchOneStatementPackRequest(statement: statement), database: database) } +} +// MARK: - Scheduling + +extension SharedReaderKey { public static func fetchAll( _ statement: S, - database: (any DatabaseReader)? = nil + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable ) -> Self - where S.QueryValue: QueryRepresentable, Self == FetchKey<[S.QueryValue.QueryOutput]>.Default { - fetch(FetchAllStatementRequest(statement: statement), database: database) + where + S.QueryValue == (), + S.Joins == (), + Self == FetchKey<[S.From.QueryOutput]>.Default + { + let statement = statement.selectStar() + return fetchAll(statement, database: database, scheduler: scheduler) } - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, - database: (any DatabaseReader)? = nil + public static func fetchAll( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable ) -> Self - where Self == FetchKey<(repeat (each Value).QueryOutput)> { - fetch(FetchOneStatementRequest(statement: statement), database: database) + where S.QueryValue: QueryRepresentable, Self == FetchKey<[S.QueryValue.QueryOutput]>.Default { + fetch(FetchAllStatementValueRequest(statement: statement), database: database, scheduler: scheduler) } public static func fetchOne( _ statement: some StructuredQueriesCore.Statement, - database: (any DatabaseReader)? = nil + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable ) -> Self where Self == FetchKey { - fetch(FetchOneStatementRequest(statement: statement), database: database) + fetch(FetchOneStatementValueRequest(statement: statement), database: database, scheduler: scheduler) } } @@ -79,11 +126,7 @@ extension SharedReaderKey { S.Joins == (repeat each J), Self == FetchKey<[(S.From.QueryOutput, repeat (each J).QueryOutput)]>.Default { - fetch( - FetchAllStatementRequest(statement: statement.selectStar()), - database: database, - scheduler: scheduler - ) + fetchAll(statement.selectStar(), database: database, scheduler: scheduler) } public static func fetchAll< @@ -99,16 +142,7 @@ extension SharedReaderKey { S.QueryValue == (V1, repeat each V2), Self == FetchKey<[(V1.QueryOutput, repeat (each V2).QueryOutput)]>.Default { - fetch(FetchAllStatementRequest(statement: statement), database: database, scheduler: scheduler) - } - - public static func fetchAll( - _ statement: S, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where S.QueryValue: QueryRepresentable, Self == FetchKey<[S.QueryValue.QueryOutput]>.Default { - fetch(FetchAllStatementRequest(statement: statement), database: database, scheduler: scheduler) + fetch(FetchAllStatementPackRequest(statement: statement), database: database, scheduler: scheduler) } public static func fetchOne( @@ -117,86 +151,89 @@ extension SharedReaderKey { scheduler: some ValueObservationScheduler & Hashable ) -> Self where Self == FetchKey<(repeat (each Value).QueryOutput)> { - fetch(FetchOneStatementRequest(statement: statement), database: database, scheduler: scheduler) - } - - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where Self == FetchKey { - fetch(FetchOneStatementRequest(statement: statement), database: database, scheduler: scheduler) + fetch(FetchOneStatementPackRequest(statement: statement), database: database, scheduler: scheduler) } } #if canImport(SwiftUI) - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { - public static func fetchAll( + public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil, animation: Animation ) -> Self where S.QueryValue == (), - S.Joins == (repeat each J), - Self == FetchKey<[(S.From.QueryOutput, repeat (each J).QueryOutput)]>.Default + S.Joins == (), + Self == FetchKey<[S.From.QueryOutput]>.Default { - fetch( - FetchAllStatementRequest(statement: statement.selectStar()), - database: database, - animation: animation - ) + let statement = statement.selectStar() + return fetchAll(statement, database: database, animation: animation) } - public static func fetchAll( - _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, + public static func fetchAll( + _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, animation: Animation ) -> Self - where Self == FetchKey<[(repeat (each Value).QueryOutput)]>.Default { + where Self == FetchKey<[Value.QueryOutput]>.Default { fetch( - FetchAllStatementRequest(statement: statement), + FetchAllStatementValueRequest(statement: statement), database: database, animation: animation ) } - public static func fetchAll( + public static func fetchOne( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, animation: Animation ) -> Self - where Self == FetchKey<[Value.QueryOutput]>.Default { + where Self == FetchKey { fetch( - FetchAllStatementRequest(statement: statement), + FetchOneStatementValueRequest(statement: statement), database: database, animation: animation ) } + } - public static func fetchOne( + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension SharedReaderKey { + public static func fetchAll( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) -> Self + where + S.QueryValue == (), + S.Joins == (repeat each J), + Self == FetchKey<[(S.From.QueryOutput, repeat (each J).QueryOutput)]>.Default + { + fetchAll(statement.selectStar(), database: database, animation: animation) + } + + public static func fetchAll( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, animation: Animation ) -> Self - where Self == FetchKey<(repeat (each Value).QueryOutput)> { + where Self == FetchKey<[(repeat (each Value).QueryOutput)]>.Default { fetch( - FetchOneStatementRequest(statement: statement), + FetchAllStatementPackRequest(statement: statement), database: database, animation: animation ) } - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement, + public static func fetchOne( + _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, animation: Animation ) -> Self - where Self == FetchKey { + where Self == FetchKey<(repeat (each Value).QueryOutput)> { fetch( - FetchOneStatementRequest(statement: statement), + FetchOneStatementPackRequest(statement: statement), database: database, animation: animation ) @@ -204,8 +241,33 @@ extension SharedReaderKey { } #endif +private struct FetchAllStatementValueRequest: FetchKeyRequest { + let statement: any StructuredQueriesCore.Statement + + func fetch(_ db: Database) throws -> [Value.QueryOutput] { + try statement.fetchAll(db) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // return AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + let lhs = lhs.statement + let rhs = rhs.statement + return AnyHashable(lhs) == AnyHashable(rhs) + } + + func hash(into hasher: inout Hasher) { + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // hasher.combine(statement) + let statement = statement + hasher.combine(statement) + } +} + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -private struct FetchAllStatementRequest: FetchKeyRequest { +private struct FetchAllStatementPackRequest: FetchKeyRequest { let statement: any StructuredQueriesCore.Statement<(repeat each Value)> func fetch(_ db: Database) throws -> [(repeat (each Value).QueryOutput)] { @@ -230,8 +292,35 @@ private struct FetchAllStatementRequest: FetchKe } } +private struct FetchOneStatementValueRequest: FetchKeyRequest { + let statement: any StructuredQueriesCore.Statement + + func fetch(_ db: Database) throws -> Value.QueryOutput { + guard let result = try statement.fetchOne(db) + else { throw NotFound() } + return result + } + + static func == (lhs: Self, rhs: Self) -> Bool { + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + let lhs = lhs.statement + let rhs = rhs.statement + return AnyHashable(lhs) == AnyHashable(rhs) + } + + func hash(into hasher: inout Hasher) { + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // hasher.combine(statement) + let statement = statement + hasher.combine(statement) + } +} + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -private struct FetchOneStatementRequest: FetchKeyRequest { +private struct FetchOneStatementPackRequest: FetchKeyRequest { let statement: any StructuredQueriesCore.Statement<(repeat each Value)> func fetch(_ db: Database) throws -> (repeat (each Value).QueryOutput) { diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index 78009eee..2809c967 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -3,40 +3,67 @@ import GRDB import SQLite3 import StructuredQueriesCore +public final class QueryValueCursor< + QueryValue: QueryRepresentable +>: QueryCursor, DatabaseCursor { + public typealias Element = QueryValue.QueryOutput + + public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { + let element = try decoder.decodeColumns(QueryValue.self) + decoder.next() + return element + } +} + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -public final class QueryCursor: DatabaseCursor { +public final class QueryPackCursor< + each QueryValue: QueryRepresentable +>: QueryCursor<(repeat (each QueryValue).QueryOutput)>, DatabaseCursor { public typealias Element = (repeat (each QueryValue).QueryOutput) + public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { + let element = try decoder.decodeColumns((repeat each QueryValue).self) + decoder.next() + return element + } +} + +final class QueryVoidCursor: QueryCursor, DatabaseCursor { + typealias Element = () + + func _element(sqliteStatement _: SQLiteStatement) throws { + try decoder.decodeColumns(Void.self) + decoder.next() + } +} + +public class QueryCursor { public var _isDone = false public let _statement: GRDB.Statement - private var decoder: SQLiteQueryDecoder + fileprivate var decoder: SQLiteQueryDecoder - init( - db: GRDB.Database, - query: some StructuredQueriesCore.Statement<(repeat each QueryValue)> - ) throws { - let query = query.query - guard !query.isEmpty else { throw EmptyQuery() } - _statement = try db.makeStatement(sql: query.string) - _statement.arguments = StatementArguments(query.bindings.map(\.databaseValue)) - decoder = SQLiteQueryDecoder( - database: db.sqliteConnection, - statement: _statement.sqliteStatement - ) + init(db: Database, query: QueryFragment) throws { + (_statement, decoder) = try db.prepare(query: query) } deinit { sqlite3_reset(_statement.sqliteStatement) } +} - public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { - let element = try decoder.decodeColumns((repeat each QueryValue).self) - decoder.next() - return element - } +private struct EmptyQuery: Error {} - private struct EmptyQuery: Error {} +extension Database { + fileprivate func prepare(query: QueryFragment) throws -> (GRDB.Statement, SQLiteQueryDecoder) { + guard !query.isEmpty else { throw EmptyQuery() } + let statement = try makeStatement(sql: query.string) + statement.arguments = StatementArguments(query.bindings.map(\.databaseValue)) + return ( + statement, + SQLiteQueryDecoder(database: sqliteConnection, statement: statement.sqliteStatement) + ) + } } extension QueryBinding { diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index 729aff2d..ff361b1c 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -2,12 +2,29 @@ import GRDB import SQLite3 import StructuredQueriesCore -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension StructuredQueriesCore.Statement { public func execute(_ db: Database) throws where QueryValue == () { + try QueryVoidCursor(db: db, query: query).next() + } + + public func fetchAll(_ db: Database) throws -> [QueryValue.QueryOutput] + where QueryValue: QueryRepresentable { + try Array(fetchCursor(db)) + } + + public func fetchOne(_ db: Database) throws -> QueryValue.QueryOutput? + where QueryValue: QueryRepresentable { try fetchCursor(db).next() } + public func fetchCursor(_ db: Database) throws -> QueryValueCursor + where QueryValue: QueryRepresentable { + try QueryValueCursor(db: db, query: query) + } +} + +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) +extension StructuredQueriesCore.Statement { public func fetchAll( _ db: Database ) throws -> [(repeat (each Value).QueryOutput)] @@ -16,11 +33,6 @@ extension StructuredQueriesCore.Statement { return try Array(cursor) } - public func fetchAll(_ db: Database) throws -> [QueryValue.QueryOutput] - where QueryValue: QueryRepresentable { - try Array(fetchCursor(db)) - } - public func fetchOne( _ db: Database ) throws -> (repeat (each Value).QueryOutput)? @@ -29,21 +41,23 @@ extension StructuredQueriesCore.Statement { return try cursor.next() } - public func fetchOne(_ db: Database) throws -> QueryValue.QueryOutput? - where QueryValue: QueryRepresentable { - try fetchCursor(db).next() - } - public func fetchCursor( _ db: Database - ) throws -> QueryCursor + ) throws -> QueryPackCursor where QueryValue == (repeat each Value) { - try QueryCursor(db: db, query: self) + try QueryPackCursor(db: db, query: query) } +} - public func fetchCursor(_ db: Database) throws -> QueryCursor - where QueryValue: QueryRepresentable { - try QueryCursor(db: db, query: self) +extension SelectStatement where QueryValue == (), Joins == () { + public func fetchAll(_ db: Database) throws -> [From.QueryOutput] { + let query = selectStar() + return try query.fetchAll(db) + } + + public func fetchOne(_ db: Database) throws -> From.QueryOutput? { + let query = selectStar() + return try query.fetchOne(db) } } From 29325975aa9e61cdf997946b76bd9b39954321e0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 14:20:28 -0700 Subject: [PATCH 057/171] wip --- .../SharingGRDB/StructuredQueries/StatementKey.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 3b88625b..66be9b64 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -9,6 +9,8 @@ import StructuredQueriesGRDBCore import SwiftUI #endif +// MARK: Basics + extension SharedReaderKey { public static func fetchAll( _ statement: S, @@ -40,6 +42,8 @@ extension SharedReaderKey { } } +// MARK: Parameter pack overloads + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { public static func fetchAll( @@ -114,6 +118,8 @@ extension SharedReaderKey { } } +// MARK: Parameter pack overloads + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { public static func fetchAll( @@ -155,6 +161,8 @@ extension SharedReaderKey { } } +// MARK: - Animation + #if canImport(SwiftUI) extension SharedReaderKey { public static func fetchAll( @@ -198,6 +206,8 @@ extension SharedReaderKey { } } + // MARK: Parameter pack overloads + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { public static func fetchAll( @@ -241,6 +251,8 @@ extension SharedReaderKey { } #endif +// MARK: - + private struct FetchAllStatementValueRequest: FetchKeyRequest { let statement: any StructuredQueriesCore.Statement From f49bd737401daf1c0e60630856fec5c995fa7767 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 14:24:41 -0700 Subject: [PATCH 058/171] wip --- .../StructuredQueries/StatementKey.swift | 72 ++++--------------- 1 file changed, 12 insertions(+), 60 deletions(-) diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 66be9b64..310306b0 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -253,98 +253,50 @@ extension SharedReaderKey { // MARK: - -private struct FetchAllStatementValueRequest: FetchKeyRequest { +private struct FetchAllStatementValueRequest: StatementKeyRequest { let statement: any StructuredQueriesCore.Statement - func fetch(_ db: Database) throws -> [Value.QueryOutput] { try statement.fetchAll(db) } - - static func == (lhs: Self, rhs: Self) -> Bool { - // NB: A Swift 6.1 regression prevents this from compiling: - // https://github.com/swiftlang/swift/issues/79623 - // return AnyHashable(lhs.statement) == AnyHashable(rhs.statement) - let lhs = lhs.statement - let rhs = rhs.statement - return AnyHashable(lhs) == AnyHashable(rhs) - } - - func hash(into hasher: inout Hasher) { - // NB: A Swift 6.1 regression prevents this from compiling: - // https://github.com/swiftlang/swift/issues/79623 - // hasher.combine(statement) - let statement = statement - hasher.combine(statement) - } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -private struct FetchAllStatementPackRequest: FetchKeyRequest { +private struct FetchAllStatementPackRequest: StatementKeyRequest { let statement: any StructuredQueriesCore.Statement<(repeat each Value)> - func fetch(_ db: Database) throws -> [(repeat (each Value).QueryOutput)] { try statement.fetchAll(db) } - - static func == (lhs: Self, rhs: Self) -> Bool { - // NB: A Swift 6.1 regression prevents this from compiling: - // https://github.com/swiftlang/swift/issues/79623 - // return AnyHashable(lhs.statement) == AnyHashable(rhs.statement) - let lhs = lhs.statement - let rhs = rhs.statement - return AnyHashable(lhs) == AnyHashable(rhs) - } - - func hash(into hasher: inout Hasher) { - // NB: A Swift 6.1 regression prevents this from compiling: - // https://github.com/swiftlang/swift/issues/79623 - // hasher.combine(statement) - let statement = statement - hasher.combine(statement) - } } -private struct FetchOneStatementValueRequest: FetchKeyRequest { +private struct FetchOneStatementValueRequest: StatementKeyRequest { let statement: any StructuredQueriesCore.Statement - func fetch(_ db: Database) throws -> Value.QueryOutput { guard let result = try statement.fetchOne(db) else { throw NotFound() } return result } - - static func == (lhs: Self, rhs: Self) -> Bool { - // NB: A Swift 6.1 regression prevents this from compiling: - // https://github.com/swiftlang/swift/issues/79623 - // AnyHashable(lhs.statement) == AnyHashable(rhs.statement) - let lhs = lhs.statement - let rhs = rhs.statement - return AnyHashable(lhs) == AnyHashable(rhs) - } - - func hash(into hasher: inout Hasher) { - // NB: A Swift 6.1 regression prevents this from compiling: - // https://github.com/swiftlang/swift/issues/79623 - // hasher.combine(statement) - let statement = statement - hasher.combine(statement) - } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -private struct FetchOneStatementPackRequest: FetchKeyRequest { +private struct FetchOneStatementPackRequest: StatementKeyRequest { let statement: any StructuredQueriesCore.Statement<(repeat each Value)> - func fetch(_ db: Database) throws -> (repeat (each Value).QueryOutput) { guard let result = try statement.fetchOne(db) else { throw NotFound() } return result } +} +private protocol StatementKeyRequest: FetchKeyRequest { + associatedtype QueryValue + var statement: any StructuredQueriesCore.Statement { get } +} + +extension StatementKeyRequest { static func == (lhs: Self, rhs: Self) -> Bool { // NB: A Swift 6.1 regression prevents this from compiling: // https://github.com/swiftlang/swift/issues/79623 - // AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + // return AnyHashable(lhs.statement) == AnyHashable(rhs.statement) let lhs = lhs.statement let rhs = rhs.statement return AnyHashable(lhs) == AnyHashable(rhs) From 1f3d7197e5a7ff17347046dbb01dfa73c1e38635 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 14:33:16 -0700 Subject: [PATCH 059/171] wip --- .../QueryCursor.swift | 22 ++++++++++--------- .../Statement+GRDB.swift | 8 +++---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index 2809c967..9d3f4861 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -3,12 +3,10 @@ import GRDB import SQLite3 import StructuredQueriesCore -public final class QueryValueCursor< - QueryValue: QueryRepresentable ->: QueryCursor, DatabaseCursor { +final class QueryValueCursor: QueryCursor { public typealias Element = QueryValue.QueryOutput - public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { + public override func _element(sqliteStatement _: SQLiteStatement) throws -> Element { let element = try decoder.decodeColumns(QueryValue.self) decoder.next() return element @@ -16,28 +14,28 @@ public final class QueryValueCursor< } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -public final class QueryPackCursor< +final class QueryPackCursor< each QueryValue: QueryRepresentable ->: QueryCursor<(repeat (each QueryValue).QueryOutput)>, DatabaseCursor { +>: QueryCursor<(repeat (each QueryValue).QueryOutput)> { public typealias Element = (repeat (each QueryValue).QueryOutput) - public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { + public override func _element(sqliteStatement _: SQLiteStatement) throws -> Element { let element = try decoder.decodeColumns((repeat each QueryValue).self) decoder.next() return element } } -final class QueryVoidCursor: QueryCursor, DatabaseCursor { +final class QueryVoidCursor: QueryCursor { typealias Element = () - func _element(sqliteStatement _: SQLiteStatement) throws { + override func _element(sqliteStatement _: SQLiteStatement) throws { try decoder.decodeColumns(Void.self) decoder.next() } } -public class QueryCursor { +public class QueryCursor: DatabaseCursor { public var _isDone = false public let _statement: GRDB.Statement @@ -50,6 +48,10 @@ public class QueryCursor { deinit { sqlite3_reset(_statement.sqliteStatement) } + + public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { + fatalError("Abstract method should be overridden in subclass") + } } private struct EmptyQuery: Error {} diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index ff361b1c..024735b6 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -17,9 +17,9 @@ extension StructuredQueriesCore.Statement { try fetchCursor(db).next() } - public func fetchCursor(_ db: Database) throws -> QueryValueCursor + public func fetchCursor(_ db: Database) throws -> QueryCursor where QueryValue: QueryRepresentable { - try QueryValueCursor(db: db, query: query) + try QueryValueCursor(db: db, query: query) } } @@ -43,9 +43,9 @@ extension StructuredQueriesCore.Statement { public func fetchCursor( _ db: Database - ) throws -> QueryPackCursor + ) throws -> QueryCursor<(repeat (each Value).QueryOutput)> where QueryValue == (repeat each Value) { - try QueryPackCursor(db: db, query: query) + try QueryPackCursor(db: db, query: query) } } From 40140ec757af35dc7ac55bfcea50e7e07600cace Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 14:38:03 -0700 Subject: [PATCH 060/171] wip --- Examples/Reminders/RemindersLists.swift | 7 ---- .../QueryCursor.swift | 38 +++++++++---------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 95d24440..d76607ff 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -109,13 +109,6 @@ struct RemindersListsView: View { } .presentationDetents([.medium]) } - .sheet(isPresented: $isAddListPresented) { - NavigationStack { - RemindersListForm() - .navigationTitle("New list") - } - .presentationDetents([.medium]) - } .searchable(text: $searchText) } diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index 9d3f4861..6ddd7443 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -3,6 +3,25 @@ import GRDB import SQLite3 import StructuredQueriesCore +public class QueryCursor: DatabaseCursor { + public var _isDone = false + public let _statement: GRDB.Statement + + fileprivate var decoder: SQLiteQueryDecoder + + init(db: Database, query: QueryFragment) throws { + (_statement, decoder) = try db.prepare(query: query) + } + + deinit { + sqlite3_reset(_statement.sqliteStatement) + } + + public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { + fatalError("Abstract method should be overridden in subclass") + } +} + final class QueryValueCursor: QueryCursor { public typealias Element = QueryValue.QueryOutput @@ -35,25 +54,6 @@ final class QueryVoidCursor: QueryCursor { } } -public class QueryCursor: DatabaseCursor { - public var _isDone = false - public let _statement: GRDB.Statement - - fileprivate var decoder: SQLiteQueryDecoder - - init(db: Database, query: QueryFragment) throws { - (_statement, decoder) = try db.prepare(query: query) - } - - deinit { - sqlite3_reset(_statement.sqliteStatement) - } - - public func _element(sqliteStatement _: SQLiteStatement) throws -> Element { - fatalError("Abstract method should be overridden in subclass") - } -} - private struct EmptyQuery: Error {} extension Database { From 4ac2900e951edb05b67d5cff8c830aa969d992ac Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 24 Mar 2025 14:53:01 -0700 Subject: [PATCH 061/171] wip --- Package.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index fa6cf78f..b8bcf9d9 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/groue/GRDB.swift", from: "7.1.0"), + .package(url: "https://github.com/groue/GRDB.swift", from: "7.4.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7844ad53..8e730bab 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "266d7b1ad94045e74884d79be79bb6458c5690d9ec3623f56a03aa7291fffd9a", + "originHash" : "eb7c38c210f418af4affa790106c484c9ebf4a016ab565d365a94e1faa778a55", "pins" : [ { "identity" : "combine-schedulers", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "6eba24d16952452a8a54f6a639491f3c8215527f", - "version" : "7.3.0" + "revision" : "52b7b7bd26821c67f1c26b5e248fd6bcac4903a7", + "version" : "7.4.0" } }, { @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "98a8f92fc40711de909171e1107b053e77003c7d" + "revision" : "0355cc1cd15f2b443a0f66cba448b3abb02a80cd" } }, { From 7ac00e9fa22d83426c839af094c6a24755ee0e9a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 27 Mar 2025 00:09:36 -0700 Subject: [PATCH 062/171] wip --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../QueryCursor.swift | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8e730bab..1da8ff5b 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "0355cc1cd15f2b443a0f66cba448b3abb02a80cd" + "revision" : "c6258592be89b940eae659cc241f0b71d2d8bf97" } }, { diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index 6ddd7443..b5a0ad16 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -60,7 +60,7 @@ extension Database { fileprivate func prepare(query: QueryFragment) throws -> (GRDB.Statement, SQLiteQueryDecoder) { guard !query.isEmpty else { throw EmptyQuery() } let statement = try makeStatement(sql: query.string) - statement.arguments = StatementArguments(query.bindings.map(\.databaseValue)) + statement.arguments = try StatementArguments(query.bindings.map { try $0.databaseValue }) return ( statement, SQLiteQueryDecoder(database: sqliteConnection, statement: statement.sqliteStatement) @@ -70,17 +70,21 @@ extension Database { extension QueryBinding { fileprivate var databaseValue: DatabaseValue { - switch self { - case let .blob(blob): - return Data(blob).databaseValue - case let .double(double): - return double.databaseValue - case let .int(int): - return int.databaseValue - case .null: - return .null - case let .text(text): - return text.databaseValue + get throws { + switch self { + case let .blob(blob): + return Data(blob).databaseValue + case let .double(double): + return double.databaseValue + case let .int(int): + return int.databaseValue + case .null: + return .null + case let .text(text): + return text.databaseValue + case let ._invalid(error): + throw error + } } } } From eb4ccd7ce5cbccb184dfebdab461b353a2c01d3c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 27 Mar 2025 14:48:16 -0700 Subject: [PATCH 063/171] wip --- Examples/Reminders/Schema.swift | 93 ++++++++++----------------------- Examples/SyncUps/Schema.swift | 56 ++++++++------------ 2 files changed, 48 insertions(+), 101 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 41af700a..8fdcf359 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -78,76 +78,37 @@ func appDatabase() throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Add reminders lists table") { db in - try #sql( - """ - CREATE TABLE "remindersLists" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "color" INTEGER NOT NULL DEFAULT '\(raw: 0x4a99ef)', - "name" TEXT NOT NULL - ) - """ - ) - .execute(db) + try db.create(table: RemindersList.tableName) { table in + table.autoIncrementedPrimaryKey("id") + table.column("color", .integer).defaults(to: 0x4a99ef).notNull() + table.column("name", .text).notNull() + } } migrator.registerMigration("Add reminders table") { db in - try #sql( - """ - CREATE TABLE "reminders" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "date" TEXT, - "isCompleted" INTEGER NOT NULL DEFAULT 0, - "isFlagged" INTEGER NOT NULL DEFAULT 0, - "remindersListID" INTEGER NOT NULL, - "notes" TEXT NOT NULL, - "priority" INTEGER, - "title" TEXT NOT NULL, - - FOREIGN KEY("remindersListID") REFERENCES "remindersLists"("id") ON DELETE CASCADE - ) - """ - ) - .execute(db) - try #sql( - """ - CREATE INDEX "reminders_remindersListID" ON "reminders"("remindersListID") - """ - ) - .execute(db) + try db.create(table: Reminder.tableName) { table in + table.autoIncrementedPrimaryKey("id") + table.column("date", .date) + table.column("isCompleted", .boolean).defaults(to: false).notNull() + table.column("isFlagged", .boolean).defaults(to: false).notNull() + table.column("notes", .text).notNull() + table.column("priority", .integer) + table.column("remindersListID", .integer) + .references(RemindersList.tableName, column: "id", onDelete: .cascade) + .notNull() + table.column("title", .text).notNull() + } } migrator.registerMigration("Add tags table") { db in - try #sql( - """ - CREATE TABLE "tags" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "name" TEXT NOT NULL COLLATE NOCASE UNIQUE - ) - """ - ) - .execute(db) - try #sql( - """ - CREATE TABLE "remindersTags" ( - "reminderID" INTEGER NOT NULL, - "tagID" INTEGER NOT NULL, - - FOREIGN KEY("reminderID") REFERENCES "reminders"("id") ON DELETE CASCADE, - FOREIGN KEY("tagID") REFERENCES "tags"("id") ON DELETE CASCADE - ) - """ - ) - .execute(db) - try #sql( - """ - CREATE INDEX "remindersTags_reminderID" ON "remindersTags"("reminderID") - """ - ) - .execute(db) - try #sql( - """ - CREATE INDEX "remindersTags_tagID" ON "remindersTags"("tagID") - """ - ) - .execute(db) + try db.create(table: Tag.tableName) { table in + table.autoIncrementedPrimaryKey("id") + table.column("name", .text).notNull().collate(.nocase).unique() + } + try db.create(table: ReminderTag.tableName) { table in + table.column("reminderID", .integer).notNull() + .references(Reminder.tableName, column: "id", onDelete: .cascade) + table.column("tagID", .integer).notNull() + .references(Tag.tableName, column: "id", onDelete: .cascade) + } } #if DEBUG migrator.registerMigration("Add mock data") { db in diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 6e54ad54..91656a77 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -100,45 +100,31 @@ func appDatabase() throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Create sync-ups table") { db in - // TODO: possible to use "\(SyncUp.id)" without table qualification? - try #sql( - """ - CREATE TABLE \(SyncUp.self) ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "seconds" INTEGER NOT NULL DEFAULT 300, - "theme" TEXT NOT NULL DEFAULT '\(raw: Theme.bubblegum.rawValue)', - "title" TEXT NOT NULL - ) - """ - ) - .execute(db) + try db.create(table: SyncUp.tableName) { table in + table.autoIncrementedPrimaryKey("id") + table.column("seconds", .integer).defaults(to: 5 * 60).notNull() + table.column("theme", .text).notNull().defaults(to: Theme.bubblegum.rawValue) + table.column("title", .text).notNull() + } } migrator.registerMigration("Create attendees table") { db in - try #sql( - """ - CREATE TABLE \(Attendee.self) ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "name" TEXT NOT NULL, - "syncUpID" INTEGER NOT NULL, - FOREIGN KEY("syncUpID") REFERENCES \(SyncUp.self)("id") ON DELETE CASCADE - ) - """ - ) - .execute(db) + try db.create(table: Attendee.tableName) { table in + table.autoIncrementedPrimaryKey("id") + table.column("name", .text).notNull() + table.column("syncUpID", .integer) + .references(SyncUp.tableName, column: "id", onDelete: .cascade) + .notNull() + } } migrator.registerMigration("Create meetings table") { db in - try #sql( - """ - CREATE TABLE \(Meeting.self) ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "date" TEXT NOT NULL UNIQUE DEFAULT CURRENT_TIMESTAMP, - "syncUpID" INTEGER NOT NULL, - "transcript" TEXT NOT NULL, - FOREIGN KEY("syncUpID") REFERENCES \(SyncUp.self)("id") ON DELETE CASCADE - ) - """ - ) - .execute(db) + try db.create(table: Meeting.tableName) { table in + table.autoIncrementedPrimaryKey("id") + table.column("date", .datetime).notNull().unique().defaults(sql: "CURRENT_TIMESTAMP") + table.column("syncUpID", .integer) + .references(SyncUp.tableName, column: "id", onDelete: .cascade) + .notNull() + table.column("transcript", .text).notNull() + } } #if DEBUG migrator.registerMigration("Insert sample data") { db in From 95fb9ce19abeb88e5197ad48a34f93a6ecdfc067 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 27 Mar 2025 17:35:50 -0700 Subject: [PATCH 064/171] wip --- Examples/CaseStudies/Animations.swift | 23 +- Examples/CaseStudies/DynamicQuery.swift | 35 +-- .../CaseStudies/ObservableModelDemo.swift | 34 ++- .../CaseStudies/SwiftDataTemplateDemo.swift | 25 +- Examples/CaseStudies/SwiftUIDemo.swift | 20 +- Examples/CaseStudies/TransactionDemo.swift | 17 +- Examples/CaseStudies/UIKitDemo.swift | 27 ++- Examples/Examples.xcodeproj/project.pbxproj | 7 + Examples/Reminders/RemindersLists.swift | 2 +- Package.swift | 8 +- README.md | 10 +- .../Articles/ComparisonWithSwiftData.md | 102 ++++---- .../Articles/DynamicQueries.md | 34 ++- .../Documentation.docc/Articles/Fetching.md | 228 ++++++++---------- .../Documentation.docc/SharingGRDB.md | 14 +- Sources/SharingGRDB/FetchKey.swift | 1 - .../StructuredQueries/StatementKey.swift | 9 + .../Statement+GRDB.swift | 7 + 18 files changed, 295 insertions(+), 308 deletions(-) diff --git a/Examples/CaseStudies/Animations.swift b/Examples/CaseStudies/Animations.swift index 00f5bbff..f8b61547 100644 --- a/Examples/CaseStudies/Animations.swift +++ b/Examples/CaseStudies/Animations.swift @@ -1,5 +1,6 @@ import Dependencies import SharingGRDB +import StructuredQueriesGRDB import SwiftUI struct AnimationsCaseStudy: SwiftUICaseStudy { @@ -13,8 +14,8 @@ struct AnimationsCaseStudy: SwiftUICaseStudy { """ let caseStudyTitle = "Animations" - @SharedReader(.fetchAll(sql: #"SELECT * FROM "facts" ORDER BY "id" DESC"#, animation: .default)) - private var facts: [Fact] + @SharedReader(.fetchAll(Fact.order { $0.id.desc() }, animation: .default)) + private var facts @Dependency(\.defaultDatabase) var database @@ -36,7 +37,12 @@ struct AnimationsCaseStudy: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - _ = try Fact(body: fact).inserted(db) + try Fact.insert { + $0.body + } values: { + fact + } + .execute(db) } } } catch {} @@ -44,13 +50,10 @@ struct AnimationsCaseStudy: SwiftUICaseStudy { } } -private struct Fact: Codable, FetchableRecord, Identifiable, MutablePersistableRecord { - static let databaseTableName = "facts" - var id: Int64? +@Table +private struct Fact: Identifiable { + let id: Int64 var body: String - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } extension DatabaseWriter where Self == DatabaseQueue { @@ -58,7 +61,7 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.databaseTableName) { table in + try db.create(table: Fact.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("body", .text).notNull() } diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index f90c5b5d..83a6aa5a 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -1,5 +1,6 @@ import Dependencies import SharingGRDB +import StructuredQueriesGRDB import SwiftUI struct DynamicQueryDemo: SwiftUICaseStudy { @@ -41,7 +42,10 @@ struct DynamicQueryDemo: SwiftUICaseStudy { .onDelete { indexSet in withErrorReporting { try database.write { db in - _ = try Fact.deleteAll(db, ids: indexSet.compactMap { facts.facts[$0].id }) + try Fact + .where{ $0.id.in(indexSet.compactMap { facts.facts[$0].id }) } + .delete() + .execute(db) } } } @@ -65,7 +69,12 @@ struct DynamicQueryDemo: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - _ = try Fact(body: fact).inserted(db) + try Fact.insert { + $0.body + } values: { + fact + } + .execute(db) } } } catch {} @@ -80,24 +89,22 @@ struct DynamicQueryDemo: SwiftUICaseStudy { var totalCount = 0 } func fetch(_ db: Database) throws -> Value { - let query = Fact.order(Column("id").desc).filter(Column("body").like("%\(query)%")) + let search = Fact + .where { $0.body.contains(query) } + .order { $0.id.desc() } return try Value( - facts: query.fetchAll(db), - searchCount: query.fetchCount(db), - totalCount: Fact.fetchCount(db) + facts: search.fetchAll(db), + searchCount: search.fetchCount(db), + totalCount: Fact.all().fetchCount(db) ) } } - } -private struct Fact: Codable, FetchableRecord, Identifiable, MutablePersistableRecord { - static let databaseTableName = "facts" - var id: Int64? +@Table +private struct Fact: Identifiable { + let id: Int var body: String - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } extension DatabaseWriter where Self == DatabaseQueue { @@ -105,7 +112,7 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.databaseTableName) { table in + try db.create(table: Fact.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("body", .text).notNull() } diff --git a/Examples/CaseStudies/ObservableModelDemo.swift b/Examples/CaseStudies/ObservableModelDemo.swift index b9d44e94..8bacbeef 100644 --- a/Examples/CaseStudies/ObservableModelDemo.swift +++ b/Examples/CaseStudies/ObservableModelDemo.swift @@ -1,5 +1,6 @@ import Dependencies import SharingGRDB +import StructuredQueriesGRDB import SwiftUI struct ObservableModelDemo: SwiftUICaseStudy { @@ -47,10 +48,10 @@ struct ObservableModelDemo: SwiftUICaseStudy { @MainActor private class Model { @ObservationIgnored - @SharedReader(.fetchAll(sql: #"SELECT * FROM "facts" ORDER BY "id" DESC"#, animation: .default)) - var facts: [Fact] + @SharedReader(.fetchAll(Fact.order { $0.id.desc() }, animation: .default)) + var facts @ObservationIgnored - @SharedReader(.fetchOne(sql: #"SELECT count(*) FROM "facts""#, animation: .default)) + @SharedReader(.fetchOne(Fact.count(), animation: .default)) var factsCount = 0 var number = 0 @@ -66,27 +67,34 @@ private class Model { as: UTF8.self ) try await database.write { db in - _ = try Fact(body: fact).inserted(db) + try Fact.insert { + $0.body + } values: { + fact + } + .execute(db) } } } func deleteFact(indices: IndexSet) { - _ = withErrorReporting { + withErrorReporting { try database.write { db in - try Fact.deleteAll(db, ids: indices.compactMap { facts[$0].id }) + try database.write { db in + try Fact + .where{ $0.id.in(indices.compactMap { facts[$0].id }) } + .delete() + .execute(db) + } } } } } -private struct Fact: Codable, FetchableRecord, Identifiable, MutablePersistableRecord { - static let databaseTableName = "facts" - var id: Int64? +@Table +private struct Fact: Identifiable { + let id: Int var body: String - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } extension DatabaseWriter where Self == DatabaseQueue { @@ -94,7 +102,7 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.databaseTableName) { table in + try db.create(table: Fact.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("body", .text).notNull() } diff --git a/Examples/CaseStudies/SwiftDataTemplateDemo.swift b/Examples/CaseStudies/SwiftDataTemplateDemo.swift index 9f2bca21..1ab1918b 100644 --- a/Examples/CaseStudies/SwiftDataTemplateDemo.swift +++ b/Examples/CaseStudies/SwiftDataTemplateDemo.swift @@ -1,4 +1,5 @@ import SharingGRDB +import StructuredQueriesGRDB import SwiftUI struct SwiftDataTemplateView: SwiftUICaseStudy { @@ -9,12 +10,12 @@ struct SwiftDataTemplateView: SwiftUICaseStudy { let caseStudyTitle = "SwiftData Template" @Dependency(\.defaultDatabase) private var database - @SharedReader(.fetch(Items(), animation: .default)) private var items + @SharedReader(.fetchAll(Item.all(), animation: .default)) private var items var body: some View { NavigationStack { List { - ForEach(items, id: \.id) { item in + ForEach(items) { item in NavigationLink { Text( "Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))" @@ -41,7 +42,7 @@ struct SwiftDataTemplateView: SwiftUICaseStudy { private func addItem() { withErrorReporting { try database.write { db in - _ = try Item(timestamp: Date()).inserted(db) + try Item.insert().execute(db) } } } @@ -49,20 +50,16 @@ struct SwiftDataTemplateView: SwiftUICaseStudy { private func deleteItems(offsets: IndexSet) { withErrorReporting { try database.write { db in - _ = try Item.deleteAll(db, keys: offsets.map { items[$0].id }) + try Item.where { $0.id.in(offsets.map { items[$0].id }) }.delete().execute(db) } } } - - private struct Items: FetchKeyRequest { - func fetch(_ db: Database) throws -> [Item] { - try Item.order(Column("timestamp").desc).fetchAll(db) - } - } } -private struct Item: Codable, Hashable, FetchableRecord, MutablePersistableRecord { - var id: Int64? +@Table +private struct Item: Identifiable { + let id: Int + @Column(as: Date.ISO8601Representation.self) var timestamp: Date } @@ -71,9 +68,9 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create items table") { db in - try db.create(table: Item.databaseTableName) { table in + try db.create(table: Item.tableName) { table in table.autoIncrementedPrimaryKey("id") - table.column("timestamp", .datetime).notNull() + table.column("timestamp", .datetime).notNull().defaults(sql: "CURRENT_TIMESTAMP") } } try! migrator.migrate(databaseQueue) diff --git a/Examples/CaseStudies/SwiftUIDemo.swift b/Examples/CaseStudies/SwiftUIDemo.swift index 5e1df872..a76c343c 100644 --- a/Examples/CaseStudies/SwiftUIDemo.swift +++ b/Examples/CaseStudies/SwiftUIDemo.swift @@ -1,5 +1,6 @@ import Dependencies import SharingGRDB +import StructuredQueriesGRDB import SwiftUI struct SwiftUIDemo: SwiftUICaseStudy { @@ -12,9 +13,9 @@ struct SwiftUIDemo: SwiftUICaseStudy { """ let caseStudyTitle = "SwiftUI Views" - @SharedReader(.fetchAll(sql: #"SELECT * FROM "facts" ORDER BY "id" DESC"#, animation: .default)) - private var facts: [Fact] - @SharedReader(.fetchOne(sql: #"SELECT count(*) FROM "facts""#, animation: .default)) + @SharedReader(.fetchAll(Fact.order { $0.id.desc() }, animation: .default)) + private var facts + @SharedReader(.fetchOne(Fact.count(), animation: .default)) var factsCount = 0 @Dependency(\.defaultDatabase) var database @@ -45,7 +46,7 @@ struct SwiftUIDemo: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - _ = try Fact(body: fact).inserted(db) + try Fact.insert { $0.body } values: { fact }.execute(db) } } } catch {} @@ -53,13 +54,10 @@ struct SwiftUIDemo: SwiftUICaseStudy { } } -private struct Fact: Codable, FetchableRecord, Identifiable, MutablePersistableRecord { - static let databaseTableName = "facts" - var id: Int64? +@Table +private struct Fact: Identifiable { + let id: Int var body: String - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } extension DatabaseWriter where Self == DatabaseQueue { @@ -67,7 +65,7 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.databaseTableName) { table in + try db.create(table: Fact.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("body", .text).notNull() } diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index 50b2da02..477f4681 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -1,4 +1,5 @@ import Dependencies +import StructuredQueriesGRDB import SharingGRDB import SwiftUI @@ -46,7 +47,7 @@ struct TransactionDemo: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - _ = try Fact(body: fact).inserted(db) + try Fact.insert { $0.body } values: { fact }.execute(db) } } } catch {} @@ -60,21 +61,19 @@ struct TransactionDemo: SwiftUICaseStudy { } func fetch(_ db: Database) throws -> Value { try Value( - facts: Fact.order(Column("id").desc).fetchAll(db), - count: Fact.fetchCount(db) + facts: Fact.order { $0.id.desc() }.fetchAll(db), + count: Fact.all().fetchCount(db) ) } } } -private struct Fact: Codable, FetchableRecord, Identifiable, MutablePersistableRecord { +@Table +private struct Fact: Identifiable { static let databaseTableName = "facts" - var id: Int64? + let id: Int64 var body: String - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } extension DatabaseWriter where Self == DatabaseQueue { @@ -82,7 +81,7 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.databaseTableName) { table in + try db.create(table: Fact.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("body", .text).notNull() } diff --git a/Examples/CaseStudies/UIKitDemo.swift b/Examples/CaseStudies/UIKitDemo.swift index 932d57e3..332f4db1 100644 --- a/Examples/CaseStudies/UIKitDemo.swift +++ b/Examples/CaseStudies/UIKitDemo.swift @@ -1,4 +1,5 @@ import SharingGRDB +import StructuredQueriesGRDB import SwiftNavigation import SwiftUI import UIKit @@ -13,8 +14,8 @@ final class UIKitCaseStudyViewController: UICollectionViewController, UIKitCaseS """ private var dataSource: UICollectionViewDiffableDataSource! - @SharedReader(.fetchAll(sql: #"SELECT * FROM "facts" ORDER BY "id" DESC"#, animation: .default)) - private var facts: [Fact] + @SharedReader(.fetchAll(Fact.order { $0.id.desc() }, animation: .default)) + private var facts private var viewDidLoadTask: Task? @Dependency(\.defaultDatabase) var database @@ -29,9 +30,9 @@ final class UIKitCaseStudyViewController: UICollectionViewController, UIKitCaseS style: .destructive, title: "Delete" ) { action, view, completion in - _ = withErrorReporting { + withErrorReporting { try database.wrappedValue.write { db in - try facts.wrappedValue[indexPath.row].delete(db) + try Fact.delete(facts.wrappedValue[indexPath.row]).execute(db) } } } @@ -96,7 +97,12 @@ final class UIKitCaseStudyViewController: UICollectionViewController, UIKitCaseS if let fact { await withErrorReporting { try await database.write { db in - _ = try Fact(body: fact).inserted(db) + try Fact.insert { + $0.body + } values: { + fact + } + .execute(db) } } } @@ -109,13 +115,10 @@ final class UIKitCaseStudyViewController: UICollectionViewController, UIKitCaseS } } -private struct Fact: Codable, FetchableRecord, Hashable, Identifiable, MutablePersistableRecord { - static let databaseTableName = "facts" - var id: Int64? +@Table +private struct Fact: Hashable, Identifiable { + let id: Int var body: String - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } extension DatabaseWriter where Self == DatabaseQueue { @@ -123,7 +126,7 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.databaseTableName) { table in + try db.create(table: Fact.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("body", .text).notNull() } diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index c144edd6..efb48c78 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ CAF3EAB92D84D85400E7E0D0 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAF3EAB82D84D85400E7E0D0 /* StructuredQueriesGRDB */; }; CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAFDD6492D5E823A00EE099E /* SharingGRDB */; }; DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = DC5FA7472D4C63D60082743E /* DependenciesMacros */; }; + DC876AFA2D9609660022207D /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DC876AF92D9609660022207D /* StructuredQueriesGRDB */; }; DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A132D4842BF0071F499 /* CasePaths */; }; DCBE8A162D4842C80071F499 /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A152D4842C80071F499 /* SharingGRDB */; }; DCF267392D48437300B680BE /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = DCF267382D48437300B680BE /* SwiftUINavigation */; }; @@ -120,6 +121,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DC876AFA2D9609660022207D /* StructuredQueriesGRDB in Frameworks */, DCF2684A2D4993BC00B680BE /* SharingGRDB in Frameworks */, CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */, ); @@ -235,6 +237,7 @@ packageProductDependencies = ( DCF268492D4993BC00B680BE /* SharingGRDB */, CA2908C82D4AF70E003F165F /* UIKitNavigation */, + DC876AF92D9609660022207D /* StructuredQueriesGRDB */, ); productName = Examples; productReference = CAF836982D4735620047AEB5 /* CaseStudies.app */; @@ -934,6 +937,10 @@ package = DC5FA7462D4C63D60082743E /* XCRemoteSwiftPackageReference "swift-dependencies" */; productName = DependenciesMacros; }; + DC876AF92D9609660022207D /* StructuredQueriesGRDB */ = { + isa = XCSwiftPackageProductDependency; + productName = StructuredQueriesGRDB; + }; DCBE8A132D4842BF0071F499 /* CasePaths */ = { isa = XCSwiftPackageProductDependency; package = DCBE8A122D4842BF0071F499 /* XCRemoteSwiftPackageReference "swift-case-paths" */; diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index d76607ff..c2366430 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -13,7 +13,7 @@ struct RemindersListsView: View { .leftJoin(Reminder.all()) { $0.id.eq($1.remindersListID) } .select { ReminderListState.Columns( - reminderCount: #sql("total(NOT \($1.isCompleted))"), + reminderCount: #sql("count(iif(\($1.isCompleted), NULL, 1))"), remindersList: $0 ) }, diff --git a/Package.swift b/Package.swift index b8bcf9d9..a194e990 100644 --- a/Package.swift +++ b/Package.swift @@ -5,10 +5,10 @@ import PackageDescription let package = Package( name: "sharing-grdb", platforms: [ - .iOS(.v16), - .macOS(.v13), - .tvOS(.v16), - .watchOS(.v10), + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v7), ], products: [ .library( diff --git a/README.md b/README.md index 9349dc65..49534a3b 100644 --- a/README.md +++ b/README.md @@ -40,19 +40,15 @@ back to the iOS 13 generation of targets.
```swift -@SharedReader( - .fetchAll( - sql: "SELECT * FROM items" - ) -) -var items: [Item] +@SharedReader(.fetchAll(Item.order(by: \.title))) +var items ``` ```swift -@Query +@Query(sort: \Item.title) var items: [Item] ``` diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md index ca19f816..be312017 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -79,8 +79,8 @@ whereas you use the `@Query` macro with SwiftData: ```swift // SharingGRDB struct ItemsView: View { - @SharedReader(.fetchAll(sql: "SELECT * FROM items")) - var items: [Item] + @SharedReader(.fetchAll(Item.order(by: \.title))) + var items var body: some View { ForEach(items) { item in @@ -94,7 +94,7 @@ whereas you use the `@Query` macro with SwiftData: ```swift // SwiftData struct ItemsView: View { - @Query + @Query(sort: \Item.title) var items: [Item] var body: some View { @@ -131,7 +131,7 @@ its functionality from scratch: @Observable class FeatureModel { @ObservationIgnored - @SharedReader(.fetchAll(sql: "SELECT * FROM items")) + @SharedReader(.fetchAll(Item.order(by: \.title)) // ... } ``` @@ -199,17 +199,17 @@ search for rows in a table: Text(item.name) } .searchable(text: $searchText) - .onChange(of: searchText) { - updateSearchQuery() + .task(id: searchText) { + await updateSearchQuery() } } func updateSearchQuery() { - $items = SharedReader( - wrappedValue: items, + await $items.load( .fetchAll( - sql: "SELECT * FROM items WHERE title LIKE ?", - arguments: ["%\(searchText)%"] + Item.where { + $0.title.contains(searchText) + } ) ) } @@ -298,8 +298,9 @@ Then, to create a new row in a table you use the `write` and `insert` methods fr // SharingGRDB @Dependency(\.defaultDatabase) var database - var newItem = Item(/* ... */) try database.write { db in + let newItem = Item(/* ... */) + try Item.insert(newItem).execute(db) try newItem.insert(db) } ``` @@ -326,7 +327,7 @@ To update an existing row you can use the `write` and `update` methods from GRDB existingItem.title = "Computer" try database.write { db in - try existingItem.update(db) + try Item.update(existingItem).execute(db) } ``` } @@ -350,7 +351,7 @@ And to delete an existing row, you can use the `write` and `delete` methods from @Dependency(\.defaultDatabase) var database try database.write { db in - try existingItem.delete(db) + try Item.deleted(existingItem).execute(db) } ``` } @@ -407,46 +408,23 @@ fetch all of the teams with their corresponding sport, you can simply perform a joins the two tables together: ```swift -struct SportWithTeamCount: Decodable, FetchableRecord { +@Selection +struct SportWithTeamCount { let sport: Sport let teamCount: Int } -let sportsWithTeamCounts = try sportsWithTeamCounts.fetchAll( - db, - Sport.annotated( - with: Sport.hasMany(Team.self).count - ) +@SharedReader( + .fetchAll( + Sport + .group(by: \.id) + .join(Team.all()) { $0.id.eq($1.sportID) } + .select { + SportWithTeamCount.Columns(sport: $0, teamCount: $1.count()) + } + ) ) -``` - -This fetches all of the sports with the number of teams in each sport, all in a single query. And -most importantly, it does not load all of the team data into memory just to compute their count. - -One can package this query up into a dedicated ``FetchKeyRequest`` like so: - -```swift -struct SportsWithTeamCounts: FetchKeyRequest { - struct Record { - let sport: Sport - let teamCount: Int - } - func fetch(_ db: Database) throws -> [Record] { - try sportsWithTeamCounts.fetchAll( - db, - Sport.annotated( - with: Sport.hasMany(Team.self).count - ) - ) - } -} -``` - -And then use this with `@SharedReader` in order to model state that is a collection of all sports -along with their corresponding team count: - -```swift -@SharedReader(.fetch(SportsWithTeamCounts())) var sportsWithTeamCounts +var sportsWithTeamCounts ``` If either of the "sports" or "teams" tables change, this query will be executed again and the @@ -473,14 +451,15 @@ Lightweight migrations in SwiftData work for simple situations, such as adding a @Column { ```swift // SharingGRDB + @Table struct Item { - var id: Int64 + let id: Int var title = "" var isInStock = true } migrator.registerMigration("Create 'items' table") { db in - db.createTable("items") { table in + db.createTable(Item.tableName) { table in table.autoIncrementedPrimaryKey("id") table.column("title", .text).notNull() table.column("isInStock", .boolean).notNull().defaults(to: true) @@ -509,16 +488,16 @@ adding a `description` field to the `Item` type: @Row { @Column { ```swift - // SharingGRDB + @Table struct Item { - var id: Int64 + let id: Int var title = "" var description = "" var isInStock = true } migrator.registerMigration("Add 'description' column to 'items'") { db in - db.alterTable("items") { table in + db.alterTable(Item.tableName) { table in table.add(column: "description", .text) } } @@ -585,14 +564,15 @@ structure of your data types. The overall steps to follow are as such: // SharingGRDB migrator.registerMigration("Make 'title' unique") { db in // 1️⃣ Delete all items that have duplicate title, keeping the first created one: - try db.execute(""" - DELETE FROM "items" - WHERE rowid NOT IN ( - SELECT min(rowid) - FROM "items" - GROUP BY "items"."title" - ) - """) + try Item + .where { + !$0.id.in( + Item + .select { $0.id.min() } + .group(by: \.title) + ) + } + .execute() // 2️⃣ Create unique index try db.create(indexOn: "items", columns: ["title"], options: .unique) } diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md b/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md index 57b97ddf..0ddf294f 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md @@ -17,7 +17,7 @@ Take the following example: ```swift struct ContentView: View { - @SharedReader(.fetchAll("SELECT * FROM items")) var items: [Item] + @SharedReader(.fetchAll(Item.all()) var items @State var filterDate: Date? @State var order: SortOrder = .reverse @@ -64,27 +64,25 @@ struct ContentView: View { private func updateQuery() async { do { - try await $items.load(.fetch(Items(filterDate: filterDate, order: order))) + try await $items.load( + .fetchAll( + Items + .where { $0.timestamp > #bind(filterDate ?? .distantPast) } + .order { + if order == .forward { + $0.timestamp + } else { + $0.timestamp.desc() + } + } + .limit(10) + ) + ) } catch { // Handle error... } } - private struct Items: FetchKeyRequest { - var filterDate: Date? - var order: SortOrder = .reverse - func fetch(_ db: Database) throws -> [Item] { - try Item.all() - .filter(Column("timestamp") > filterDate ?? .distantPast) - .sort( - order == .forward - ? Column("timestamp") - : Column("timestamp").desc - ) - .limit(10) - } - } - // ... } ``` @@ -94,5 +92,5 @@ struct ContentView: View { > locally to this view, we use `@State.SharedReader`, instead, which wraps a `@SharedReader` in > SwiftUI `@State`. -> Note: We are using the ``Sharing/SharedReaderKey/fetch(_:database:animation:)-rgj4`` style of +> Note: We are using the ``Sharing/SharedReaderKey/fet`` style of > querying the database. See for more APIs that can be used. diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md index c6856cac..8432ac1a 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md @@ -4,183 +4,132 @@ Learn about the various tools for fetching data from a SQLite database. ## Overview -All data fetching happens by providing -[`fetchAll`](), -[`fetchOne`](), or - [`fetch`](), to the `@SharedReader` -property wrapper. The primary differences between these choices is whether you want to specify your -query as a raw SQL string, or as a query built with GRDB's query building tools. +All data fetching happens by providing the `fetchAll`, `fetchOne`, or `fetch` key to the +`@SharedReader` property wrapper. The primary differences between these choices is whether you want +to build queries with [Structured Queries][structured-queries-gh], specify your query as a raw SQL +string, or if you want to assemble your value from one or more queries using a raw database +connection. + * [Querying with Structured Queries](#Querying-with-Structured-Queries) * [Querying with SQL](#Querying-with-SQL) - * [Querying with a SQL builder](#Querying-with-a-SQL-builder) - * [Multiple queries in a single transaction](#Multiple-queries-in-a-single-transaction) + * [Querying with custom request](#Querying-with-a-custom-request) + +[structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries ### Querying with SQL -For simple queries it can often be very convenient to specify how you want to fetch data from SQLite -as a raw SQL query string. For example, if you simply want to fetch all records from a table, you -can do so using the -[`fetchAll`]() key: +[Structured Queries][structured-queries-gh] is a library for building type-safe queries that safely +and performantly decode into Swift data types. For example, if you simply want to fetch all records +from a table, you can do so by plugging the query directly into +[`fetchAll`](): ```swift -@SharedReader(.fetchAll(sql: "SELECT * FROM items")) var items: [Item] +@SharedReader(.fetchAll(Item.all()) +var items ``` And if you want to sort the results, you can do so with an ordering clause: ```swift -@SharedReader(.fetchAll(sql: "SELECT * FROM items ORDER BY createdAt DESC")) -var items: [Item] +@SharedReader(.fetchAll(Item.order { $0.createdAt.desc() })) +var items ``` Or, if you want to only compute an aggregate of the data in a table, such as the count of the rows, you can do so using the -[`fetchOne`]() key: +[`fetchOne`]() key: ```swift -@SharedReader(.fetchOne(sql: "SELECT count(*) FROM items")) +@SharedReader(.fetchOne(Item.count())) var itemsCount = 0 ``` -In each of these cases, the query is so simple that you may prefer writing the SQL yourself. -After all, the data is ultimately stored in SQLite, and so being very familiar with how SQL works is -only beneficial to you. - -However, there are some downsides to this. First, complex queries can get very noisy at the call -site, such as if you have filtering logic: +While Structured Queries' builder is powerful, it is also stricter than SQLite, which will happily +coerce any data into any type, and some queries are more conveniently expressed through these +coercions. Structured Queries should never get in your way, so rather than describe to the Swift +type system every explicit cast and coalesce, you can always embed SQL directly in a query using +the `#sql` macro: ```swift -@SharedReader( - .fetchAll( - sql: """ - SELECT * FROM items - WHERE NOT isInStock - ORDER BY createdAt DESC - """ - ) -) -var outOfStockItems: [Item] +@SharedReader(.fetchAll(Item.where { #sql("\($0.createdAt) > date('now', '-7 days')") })) +var items ``` -And second, and most egregious, is that a raw string is susceptible to typos and the compiler cannot -help us write valid SQL code. For example, if we accidentally specified "`ORDER`" instead of -"`ORDER BY`": +The `#sql` macro will safely bind any input and even perform basic syntax validation. + +You can even use `#sql` to write the entire query: ```swift @SharedReader( - .fetchAll( - sql: """ - SELECT * FROM items - WHERE NOT isInStock - ORDER createdAt DESC - """ + #sql( + """ + SELECT \(Item.columns) FROM \(Item.self) + WHERE \(Item.createdAt) > date('now', '-7 days') + """ ) ) -var outOfStockItems: [Item] +var items: [Item] ``` -…then this will compile just fine but will be a runtime error. And further, you will often need to -build up a complex query from various settings in your feature, and doing so will rely on messy -string interpolation. +The choice is up to you for each query or query fragment. To learn more, see the +[Structured Queries documentation][structured-queries-docs]. -For these reasons, and more, people turn to query builders. +[structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries +[structured-queries-docs]: #TODO -### Querying with a SQL builder +### Querying with raw SQL -The GRDB library comes with a set of [query building tools][query-interface] that allow one to build -SQL statements in a safer manner. For example, something as simple as: +SharingGRDB also comes with a more basic set of tools that work directly with GRDB. This includes +a [`fetchAll`]() key that takes a raw +SQL string: ```swift -let query = Item.all() -``` - -…represents the SQL statement: - -```sql -SELECT * FROM items -``` - -And the following: - -```swift -let query = Item.all() - .filter(!Column("isInStock")) - .order(Column("createdAt").desc) -``` - -…represents the SQL statement: - -```sql -SELECT * FROM items -WHERE NOT isInStock -ORDER BY createdAt DESC +@SharedReader(.fetchAll(sql: "SELECT * FROM items")) var items: [Item] ``` -Some may prefer writing their SQL statements in this style rather than a raw SQL string, but we -always recommend being fully familiar with the underlying SQL being generated by the builder. - -Unfortunately, one cannot use this query builder directly with the `@SharedReader` like this: +As well as a [`fetchOne`]() key: ```swift -@SharedReader(.fetch(Item.all())) var items 🛑 +@SharedReader(.fetchOne(sql: "SELECT count(*) FROM items")) +var itemsCount = 0 ``` -This is because the query builder does not provide a unique, `Hashable` identity, which is necessary -to be used with `@SharedReader`. To work around this, one simply defines a conformance to our -``FetchKeyRequest`` protocol, which requires hashability, and in that conformance one can use -the builder tools to query the database: - -```swift -struct Items: FetchKeyRequest { - func fetch(_ db: Database) throws -> [Item] { - try Item.all() - .filter(!Column("isInStock")) - .order(Column("createdAt").desc) - .fetch(db) - } -} -``` - -With this conformance defined one can use -[`fetch`]() key to execute the -query specified by the `Items` type: +These APIs simply feed their data directly to GRDB's equivalent `Database` APIs, which means it is +up to you to safely bind arguments and avoid SQL injection. If you want to write SQL queries by +hand, consider using Structured Queries' `#sql` macro, instead. -```swift -@SharedReader(.fetch(Items()) var items -``` +> Note: While GRDB comes with its own query builder, its lack of hashable identity means that it is +> not directly compatible with `@SharedReader`. It is for this reason SharingGRDB includes bindings +> to [Structured Queries][structured-queries-gh], a powerful SQL builder library that preserves a +> hashable identity for its queries. -> Note: Because of the type information available to `Items`, the type and default value can be -> omitted from the declaration of `items`. +[structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries -Typically the conformances to ``FetchKeyRequest`` can even be made private and nested inside -whatever type they are used in, such as SwiftUI view, `@Observable` model, or UIKit view controller. -The only time it needs to be made public is if it's shared amongst many features. +### Querying with custom requests -### Multiple queries in a single transaction +It is also possible to fetch data for a `@SharedReader` from a database connection. This can be +useful if you want to perform several queries in a single database transaction: -Querying with ``FetchKeyRequest`` has the added benefit of being able to execute multiple queries in -a single database transaction. Right now each instance of `@SharedReader` in a feature executes -each of their queries in a separate transaction. So, if we wanted to query for all in stock -items, as well as the count of all items (in stock plus out of stock) like so: +Each instance of `@SharedReader` in a feature executes each of their queries in a separate +transaction. So, if we wanted to query for all in-stock items, as well as the count of all items +(in-stock plus out-of-stock) like so: ```swift -@SharedReader(.fetchOne(sql: "SELECT count(*) FROM items")) +@SharedReader(.fetchOne(Item.count())) var itemsCount = 0 -@SharedReader(.fetchAll(sql: "SELECT * FROM items WHERE isInStock")) -var inStockItems: [Item] +@SharedReader(.fetchAll(Item.where(\.isInStock))) +var inStockItems ``` …this is technically 2 queries run in 2 separate database transactions. Often this can be just fine, but if you have multiple queries that tend to change at the same time (_e.g._, when items are created or deleted, `itemsCount` and `inStockItems` will change -at the same time), then you can use ``FetchKeyRequest`` to bundle these two queries into a single -transaction. +at the same time), then you can bundle these two queries into a single transaction. -To do this, define a ``FetchKeyRequest/Value`` type inside the conformance that represents all the -data you want to query for, and then construct it inside the ``FetchKeyRequest/fetch(_:)`` -method: +To do this, one simply defines a conformance to our ``FetchKeyRequest`` protocol, and in that +conformance one can use the builder tools to query the database: ```swift struct Items: FetchKeyRequest { @@ -190,21 +139,24 @@ struct Items: FetchKeyRequest { } func fetch(_ db: Database) throws -> Value { try Value( - inStockItems: Item.all().filter(Column("isInStock")).fetchAll(db), + inStockItems: Item.where(\.isInStock).fetchAll(db), itemsCount: Item.fetchCount(db) ) } } ``` -This selects the in-stock items and the total count of items as two queries inside a single database -transaction. +Here we have defined a ``FetchKeyRequest/Value`` type inside the conformance that represents all the +data we want to query for in a single transaction, and then we can construct it and return it from +the ``FetchKeyRequest/fetch(_:)`` method. -Then you can use this key just as you did before, but now you can access the `inStockItems` and -`itemsCount` properties to access the queried data: +With this conformance defined we can use +[`fetch`]() key to execute the query specified +by the `Items` type, and we can access the `inStockItems` and `itemsCount` properties to get to the +queried data: ```swift -@SharedReader(.fetch(Items())) var items = Items.Value() +@SharedReader(.fetch(Items()) var items = Items.Value() items.inStockItems // [Item(/* ... */), /* ... */] items.itemsCount // 100 ``` @@ -212,4 +164,30 @@ items.itemsCount // 100 > Note: A default must be provided to `@SharedReader` since it is querying for a custom data type > instead of a collection of data. -[query-interface]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/queryinterface +You can perform any kind of work and return any kind of data from ``FetchKeyRequest/fetch(_:)``, +which means if you have existing code exercising GRDB APIs, they are immediately usable. For +example, you may have some code that is using GRDB's built-in query builder: + +```swift +let query = Item.all() + .filter(!Column("isInStock")) + .order(Column("createdAt").desc) +``` + +Well there is no need to rewrite this code. Because `fetch` is handed a GRDB database connection, +you are free to use it however you please: + +```swift +struct Items: FetchKeyRequest { + func fetch(_ db: Database) throws -> [Item] { + try Item.all() + .filter(!Column("isInStock")) + .order(Column("createdAt").desc) + .fetch(db) + } +} +``` + +Typically the conformances to ``FetchKeyRequest`` can even be made private and nested inside +whatever type they are used in, such as SwiftUI view, `@Observable` model, or UIKit view controller. +The only time it needs to be made public is if it's shared amongst many features. diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index aaa717dc..5279e52f 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -9,10 +9,8 @@ back to the iOS 13 generation of targets. @Column { ```swift // SharingGRDB - @SharedReader( - .fetchAll(sql: "SELECT * FROM items") - ) - var items: [Item] + @SharedReader(.fetch(Item.all()) + var items ``` } @Column { @@ -83,8 +81,8 @@ This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies [`fetchAll`](): ```swift -@SharedReader(.fetchAll(sql: "SELECT * FROM items")) -var items: [Item] +@SharedReader(.fetch(Item.all())) +var items ``` And you can access this database throughout your application in a way similar to how one accesses @@ -96,9 +94,9 @@ a model context, via a property wrapper: // SharingGRDB @Dependency(\.defaultDatabase) var database - var newItem = Item(/* ... */) try database.write { db in - try newItem.insert(db) + let newItem = Item(/* ... */) + try Item.insert(newItem).execute(db) } ``` } diff --git a/Sources/SharingGRDB/FetchKey.swift b/Sources/SharingGRDB/FetchKey.swift index f7dd5d4c..2bbcc6b0 100644 --- a/Sources/SharingGRDB/FetchKey.swift +++ b/Sources/SharingGRDB/FetchKey.swift @@ -397,7 +397,6 @@ private struct FetchOne: FetchKeyRequest { } } -// TODO: Better name or somewhere else to nest? public struct NotFound: Error { public init() {} } diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 310306b0..d3447151 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -46,6 +46,7 @@ extension SharedReaderKey { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { + @_disfavoredOverload public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil @@ -58,6 +59,7 @@ extension SharedReaderKey { fetchAll(statement.selectStar(), database: database) } + @_disfavoredOverload public static func fetchAll< S: SelectStatement, V1: QueryRepresentable, @@ -73,6 +75,7 @@ extension SharedReaderKey { fetch(FetchAllStatementPackRequest(statement: statement), database: database) } + @_disfavoredOverload public static func fetchOne( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil @@ -122,6 +125,7 @@ extension SharedReaderKey { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { + @_disfavoredOverload public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil, @@ -135,6 +139,7 @@ extension SharedReaderKey { fetchAll(statement.selectStar(), database: database, scheduler: scheduler) } + @_disfavoredOverload public static func fetchAll< S: SelectStatement, V1: QueryRepresentable, @@ -151,6 +156,7 @@ extension SharedReaderKey { fetch(FetchAllStatementPackRequest(statement: statement), database: database, scheduler: scheduler) } + @_disfavoredOverload public static func fetchOne( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, @@ -210,6 +216,7 @@ extension SharedReaderKey { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { + @_disfavoredOverload public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil, @@ -223,6 +230,7 @@ extension SharedReaderKey { fetchAll(statement.selectStar(), database: database, animation: animation) } + @_disfavoredOverload public static func fetchAll( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, @@ -236,6 +244,7 @@ extension SharedReaderKey { ) } + @_disfavoredOverload public static func fetchOne( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index 024735b6..3c70368f 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -49,6 +49,13 @@ extension StructuredQueriesCore.Statement { } } +extension SelectStatement where QueryValue == (), Joins == () { + public func fetchCount(_ db: Database) throws -> Int { + let query = all().count() + return try query.fetchOne(db) ?? 0 + } +} + extension SelectStatement where QueryValue == (), Joins == () { public func fetchAll(_ db: Database) throws -> [From.QueryOutput] { let query = selectStar() From 783b6877f5b684129bbc9d4420a885d2cc3244a7 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 31 Mar 2025 10:13:04 -0700 Subject: [PATCH 065/171] wip --- Examples/Reminders/RemindersLists.swift | 2 +- Examples/Reminders/Schema.swift | 67 +++++++++++++++---------- Examples/SyncUps/Schema.swift | 51 +++++++++++-------- Package.resolved | 15 ++++-- 4 files changed, 83 insertions(+), 52 deletions(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index c2366430..f79d34d8 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -13,7 +13,7 @@ struct RemindersListsView: View { .leftJoin(Reminder.all()) { $0.id.eq($1.remindersListID) } .select { ReminderListState.Columns( - reminderCount: #sql("count(iif(\($1.isCompleted), NULL, 1))"), + reminderCount: #sql("count(iif(\($1.isCompleted), NULL, \($1.id)))"), remindersList: $0 ) }, diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 8fdcf359..547a80d2 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -78,37 +78,50 @@ func appDatabase() throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Add reminders lists table") { db in - try db.create(table: RemindersList.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("color", .integer).defaults(to: 0x4a99ef).notNull() - table.column("name", .text).notNull() - } + try #sql(""" + CREATE TABLE "remindersLists" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "color" INTEGER NOT NULL DEFAULT \(raw: 0x4a99ef), + "name" TEXT NOT NULL + ) + """) + .execute(db) } migrator.registerMigration("Add reminders table") { db in - try db.create(table: Reminder.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("date", .date) - table.column("isCompleted", .boolean).defaults(to: false).notNull() - table.column("isFlagged", .boolean).defaults(to: false).notNull() - table.column("notes", .text).notNull() - table.column("priority", .integer) - table.column("remindersListID", .integer) - .references(RemindersList.tableName, column: "id", onDelete: .cascade) - .notNull() - table.column("title", .text).notNull() - } + try #sql(""" + CREATE TABLE "reminders" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "date" TEXT, + "isCompleted" INTEGER NOT NULL DEFAULT 0, + "isFlagged" INTEGER NOT NULL DEFAULT 0, + "notes" TEXT, + "priority" INTEGER, + "remindersListID" INTEGER NOT NULL, + "title" TEXT NOT NULL, + + FOREIGN KEY("remindersListID") REFERENCES "remindersLists"("id") ON DELETE CASCADE + ) + """) + .execute(db) } migrator.registerMigration("Add tags table") { db in - try db.create(table: Tag.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("name", .text).notNull().collate(.nocase).unique() - } - try db.create(table: ReminderTag.tableName) { table in - table.column("reminderID", .integer).notNull() - .references(Reminder.tableName, column: "id", onDelete: .cascade) - table.column("tagID", .integer).notNull() - .references(Tag.tableName, column: "id", onDelete: .cascade) - } + try #sql(""" + CREATE TABLE "tags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL COLLATE NOCASE UNIQUE + ) + """) + .execute(db) + try #sql(""" + CREATE TABLE "remindersTags" ( + "reminderID" INTEGER NOT NULL, + "tagID" INTEGER NOT NULL, + + FOREIGN KEY("reminderID") REFERENCES "reminders"("id") ON DELETE CASCADE, + FOREIGN KEY("tagID") REFERENCES "tags"("id") ON DELETE CASCADE + ) + """) + .execute(db) } #if DEBUG migrator.registerMigration("Add mock data") { db in diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 91656a77..3e46a11f 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -100,31 +100,40 @@ func appDatabase() throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Create sync-ups table") { db in - try db.create(table: SyncUp.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("seconds", .integer).defaults(to: 5 * 60).notNull() - table.column("theme", .text).notNull().defaults(to: Theme.bubblegum.rawValue) - table.column("title", .text).notNull() - } + try #sql(""" + CREATE TABLE "syncUps" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "seconds" INTEGER NOT NULL DEFAULT 300, + "theme" TEXT NOT NULL DEFAULT \(raw: Theme.bubblegum.rawValue), + "title" TEXT NOT NULL + ) + """) + .execute(db) } migrator.registerMigration("Create attendees table") { db in - try db.create(table: Attendee.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("name", .text).notNull() - table.column("syncUpID", .integer) - .references(SyncUp.tableName, column: "id", onDelete: .cascade) - .notNull() - } + try #sql(""" + CREATE TABLE "attendees" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "syncUpID" INTEGER NOT NULL, + + FOREIGN KEY("syncUpID") REFERENCES "syncUps"("id") ON DELETE CASCADE + ) + """) + .execute(db) } migrator.registerMigration("Create meetings table") { db in - try db.create(table: Meeting.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("date", .datetime).notNull().unique().defaults(sql: "CURRENT_TIMESTAMP") - table.column("syncUpID", .integer) - .references(SyncUp.tableName, column: "id", onDelete: .cascade) - .notNull() - table.column("transcript", .text).notNull() - } + try #sql(""" + CREATE TABLE "meetings" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "date" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP UNIQUE, + "syncUpID" INTEGER NOT NULL, + "transcript" TEXT NOT NULL, + + FOREIGN KEY("syncUpID") REFERENCES "syncUps"("id") ON DELETE CASCADE + ) + """) + .execute(db) } #if DEBUG migrator.registerMigration("Insert sample data") { db in diff --git a/Package.resolved b/Package.resolved index 3a20f47f..2f2af14d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c33e1f57b8a5fc20bfaec1c9a156c28901b2b3351b9aa7a3ec82114782ad65fb", + "originHash" : "4d4d6d7803aca8d0e77b7efabbe6ce405596c64d2acedbcb7c189af5533bf81a", "pins" : [ { "identity" : "combine-schedulers", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "6eba24d16952452a8a54f6a639491f3c8215527f", - "version" : "7.3.0" + "revision" : "04e73c26c4ce8218ab85aaf791942bb0b204f330", + "version" : "7.4.1" } }, { @@ -109,6 +109,15 @@ "version" : "2.3.3" } }, + { + "identity" : "swift-structured-queries", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-structured-queries", + "state" : { + "branch" : "main", + "revision" : "02e206f24eef3af0b5321d08a75510ca04cf2215" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", From 3e3707edea2a58de492a46df80d50f977e8a9781 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 31 Mar 2025 10:14:57 -0700 Subject: [PATCH 066/171] wip --- Examples/CaseStudies/TransactionDemo.swift | 1 - Examples/Reminders/RemindersLists.swift | 61 ++++---- Package.resolved | 141 ------------------ .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Documentation.docc/Articles/Fetching.md | 9 +- .../QueryCursor.swift | 22 ++- .../SQLiteQueryDecoder.swift | 20 +-- .../Statement+GRDB.swift | 16 +- 8 files changed, 83 insertions(+), 191 deletions(-) delete mode 100644 Package.resolved diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index 477f4681..1e946563 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -66,7 +66,6 @@ struct TransactionDemo: SwiftUICaseStudy { ) } } - } @Table diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index f79d34d8..79e0146b 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -6,6 +6,22 @@ import StructuredQueries import SwiftUI struct RemindersListsView: View { + @Selection + fileprivate struct ReminderListState: Identifiable { + var id: RemindersList.ID { remindersList.id } + var reminderCount: Int + var remindersList: RemindersList + } + + @Selection + fileprivate struct Stats { + var allCount = 0 + var completedCount = 0 + var flaggedCount = 0 + var scheduledCount = 0 + var todayCount = 0 + } + @SharedReader( .fetchAll( RemindersList @@ -21,7 +37,21 @@ struct RemindersListsView: View { ) ) private var remindersLists - @SharedReader(.fetch(Stats())) private var stats = Stats.Value() + + @SharedReader( + .fetchOne( + Reminder.select { + Stats.Columns( + allCount: $0.count(), + completedCount: $0.count(filter: $0.isCompleted), + flaggedCount: $0.count(filter: $0.isFlagged), + scheduledCount: $0.count(filter: #sql("date(\($0.date)) > date('now')")), + todayCount: $0.count(filter: #sql("date(\($0.date)) = date('now')")) + ) + } + ) + ) + private var stats = Stats() @State private var isAddListPresented = false @State private var searchText = "" @@ -111,35 +141,6 @@ struct RemindersListsView: View { } .searchable(text: $searchText) } - - @Selection - fileprivate struct ReminderListState: Identifiable { - var id: RemindersList.ID { remindersList.id } - var reminderCount: Int - var remindersList: RemindersList - } - fileprivate struct Stats: FetchKeyRequest { - func fetch(_ db: Database) throws -> Value { - try Value( - allCount: Reminder.count().fetchOne(db) ?? 0, - completedCount: Reminder.where(\.isCompleted).count().fetchOne(db) ?? 0, - flaggedCount: Reminder.where(\.isFlagged).count().fetchOne(db) ?? 0, - scheduledCount: Reminder.count() - .where { #sql("date(\($0.date)) > date('now')") } - .fetchOne(db) ?? 0, - todayCount: Reminder.count() - .where { #sql("date(\($0.date)) = date('now')") } - .fetchOne(db) ?? 0 - ) - } - struct Value { - var allCount = 0 - var completedCount = 0 - var flaggedCount = 0 - var scheduledCount = 0 - var todayCount = 0 - } - } } private struct ReminderGridCell: View { diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 2f2af14d..00000000 --- a/Package.resolved +++ /dev/null @@ -1,141 +0,0 @@ -{ - "originHash" : "4d4d6d7803aca8d0e77b7efabbe6ce405596c64d2acedbcb7c189af5533bf81a", - "pins" : [ - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "5928286acce13def418ec36d05a001a9641086f2", - "version" : "1.0.3" - } - }, - { - "identity" : "grdb.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRDB.swift", - "state" : { - "revision" : "04e73c26c4ce8218ab85aaf791942bb0b204f330", - "version" : "7.4.1" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", - "version" : "1.0.6" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", - "version" : "1.1.4" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", - "version" : "1.3.1" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", - "version" : "1.3.3" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "ec2862d1364536fc22ec56a3094e7a034bbc7da8", - "version" : "1.8.1" - } - }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-plugin", - "state" : { - "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", - "version" : "1.4.3" - } - }, - { - "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", - "location" : "https://github.com/pointfreeco/swift-identified-collections", - "state" : { - "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", - "version" : "1.1.1" - } - }, - { - "identity" : "swift-perception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-perception", - "state" : { - "revision" : "671fa54b279fd73933b4a8b34782ebf6c8869145", - "version" : "1.5.1" - } - }, - { - "identity" : "swift-sharing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-sharing", - "state" : { - "revision" : "2c840cf2ae0526ad6090e7796c4e13d9a2339f4a", - "version" : "2.3.3" - } - }, - { - "identity" : "swift-structured-queries", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-structured-queries", - "state" : { - "branch" : "main", - "revision" : "02e206f24eef3af0b5321d08a75510ca04cf2215" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax", - "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", - "version" : "1.5.2" - } - } - ], - "version" : 3 -} diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1da8ff5b..1c03d192 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "eb7c38c210f418af4affa790106c484c9ebf4a016ab565d365a94e1faa778a55", + "originHash" : "7939737e84a296cb06977fbf69313478a9a8e3276c896f8a388f1eba840eeb84", "pins" : [ { "identity" : "combine-schedulers", @@ -133,7 +133,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "c6258592be89b940eae659cc241f0b71d2d8bf97" + "revision" : "7db4780ab7f5bd9c434623512fadabb9df413788" } }, { diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md index 8432ac1a..98e2ad15 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md @@ -16,13 +16,15 @@ connection. [structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries -### Querying with SQL +### Querying with Structured Queries [Structured Queries][structured-queries-gh] is a library for building type-safe queries that safely and performantly decode into Swift data types. For example, if you simply want to fetch all records from a table, you can do so by plugging the query directly into [`fetchAll`](): +@Comment { TODO: Add '@Table' definition? } + ```swift @SharedReader(.fetchAll(Item.all()) var items @@ -79,6 +81,11 @@ The choice is up to you for each query or query fragment. To learn more, see the ### Querying with raw SQL +@Comment { + TODO: Call out why these tools exist (to allow one to avoid the Swift Syntax cost of building a + macro) +} + SharingGRDB also comes with a more basic set of tools that work directly with GRDB. This includes a [`fetchAll`]() key that takes a raw SQL string: diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index b5a0ad16..4427b8ab 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -7,8 +7,10 @@ public class QueryCursor: DatabaseCursor { public var _isDone = false public let _statement: GRDB.Statement - fileprivate var decoder: SQLiteQueryDecoder + @usableFromInline + var decoder: SQLiteQueryDecoder + @usableFromInline init(db: Database, query: QueryFragment) throws { (_statement, decoder) = try db.prepare(query: query) } @@ -22,9 +24,11 @@ public class QueryCursor: DatabaseCursor { } } +@usableFromInline final class QueryValueCursor: QueryCursor { public typealias Element = QueryValue.QueryOutput + @inlinable public override func _element(sqliteStatement _: SQLiteStatement) throws -> Element { let element = try decoder.decodeColumns(QueryValue.self) decoder.next() @@ -33,11 +37,13 @@ final class QueryValueCursor: QueryCursor: QueryCursor<(repeat (each QueryValue).QueryOutput)> { public typealias Element = (repeat (each QueryValue).QueryOutput) + @inlinable public override func _element(sqliteStatement _: SQLiteStatement) throws -> Element { let element = try decoder.decodeColumns((repeat each QueryValue).self) decoder.next() @@ -45,19 +51,26 @@ final class QueryPackCursor< } } +@usableFromInline final class QueryVoidCursor: QueryCursor { typealias Element = () + @inlinable override func _element(sqliteStatement _: SQLiteStatement) throws { try decoder.decodeColumns(Void.self) decoder.next() } } -private struct EmptyQuery: Error {} +@usableFromInline +struct EmptyQuery: Error { + @usableFromInline + init() {} +} extension Database { - fileprivate func prepare(query: QueryFragment) throws -> (GRDB.Statement, SQLiteQueryDecoder) { + @inlinable + func prepare(query: QueryFragment) throws -> (GRDB.Statement, SQLiteQueryDecoder) { guard !query.isEmpty else { throw EmptyQuery() } let statement = try makeStatement(sql: query.string) statement.arguments = try StatementArguments(query.bindings.map { try $0.databaseValue }) @@ -69,7 +82,8 @@ extension Database { } extension QueryBinding { - fileprivate var databaseValue: DatabaseValue { + @inlinable + var databaseValue: DatabaseValue { get throws { switch self { case let .blob(blob): diff --git a/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift index eb8ab2ee..feb260fe 100644 --- a/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift +++ b/Sources/StructuredQueriesGRDBCore/SQLiteQueryDecoder.swift @@ -1,24 +1,29 @@ import SQLite3 import StructuredQueriesCore +@usableFromInline struct SQLiteQueryDecoder: QueryDecoder { - private let database: OpaquePointer? - private let statement: OpaquePointer - private var currentIndex: Int32 = 0 + @usableFromInline + let database: OpaquePointer? + @usableFromInline + let statement: OpaquePointer + + @usableFromInline + var currentIndex: Int32 = 0 + + @usableFromInline init(database: OpaquePointer?, statement: OpaquePointer) { self.database = database self.statement = statement } @inlinable - @inline(__always) mutating func next() { currentIndex = 0 } @inlinable - @inline(__always) mutating func decode(_ columnType: [UInt8].Type) throws -> [UInt8]? { defer { currentIndex += 1 } guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } @@ -31,7 +36,6 @@ struct SQLiteQueryDecoder: QueryDecoder { } @inlinable - @inline(__always) mutating func decode(_ columnType: Double.Type) throws -> Double? { defer { currentIndex += 1 } guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } @@ -39,7 +43,6 @@ struct SQLiteQueryDecoder: QueryDecoder { } @inlinable - @inline(__always) mutating func decode(_ columnType: Int64.Type) throws -> Int64? { defer { currentIndex += 1 } guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } @@ -47,7 +50,6 @@ struct SQLiteQueryDecoder: QueryDecoder { } @inlinable - @inline(__always) mutating func decode(_ columnType: String.Type) throws -> String? { defer { currentIndex += 1 } guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } @@ -55,13 +57,11 @@ struct SQLiteQueryDecoder: QueryDecoder { } @inlinable - @inline(__always) mutating func decode(_ columnType: Bool.Type) throws -> Bool? { try decode(Int64.self).map { $0 != 0 } } @inlinable - @inline(__always) mutating func decode(_ columnType: Int.Type) throws -> Int? { try decode(Int64.self).map(Int.init) } diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index 3c70368f..fbe1c7f9 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -3,20 +3,24 @@ import SQLite3 import StructuredQueriesCore extension StructuredQueriesCore.Statement { + @inlinable public func execute(_ db: Database) throws where QueryValue == () { try QueryVoidCursor(db: db, query: query).next() } + @inlinable public func fetchAll(_ db: Database) throws -> [QueryValue.QueryOutput] where QueryValue: QueryRepresentable { try Array(fetchCursor(db)) } + @inlinable public func fetchOne(_ db: Database) throws -> QueryValue.QueryOutput? where QueryValue: QueryRepresentable { try fetchCursor(db).next() } + @inlinable public func fetchCursor(_ db: Database) throws -> QueryCursor where QueryValue: QueryRepresentable { try QueryValueCursor(db: db, query: query) @@ -25,6 +29,7 @@ extension StructuredQueriesCore.Statement { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension StructuredQueriesCore.Statement { + @inlinable public func fetchAll( _ db: Database ) throws -> [(repeat (each Value).QueryOutput)] @@ -33,6 +38,7 @@ extension StructuredQueriesCore.Statement { return try Array(cursor) } + @inlinable public func fetchOne( _ db: Database ) throws -> (repeat (each Value).QueryOutput)? @@ -41,6 +47,7 @@ extension StructuredQueriesCore.Statement { return try cursor.next() } + @inlinable public func fetchCursor( _ db: Database ) throws -> QueryCursor<(repeat (each Value).QueryOutput)> @@ -50,6 +57,7 @@ extension StructuredQueriesCore.Statement { } extension SelectStatement where QueryValue == (), Joins == () { + @inlinable public func fetchCount(_ db: Database) throws -> Int { let query = all().count() return try query.fetchOne(db) ?? 0 @@ -57,19 +65,22 @@ extension SelectStatement where QueryValue == (), Joins == () { } extension SelectStatement where QueryValue == (), Joins == () { + @inlinable public func fetchAll(_ db: Database) throws -> [From.QueryOutput] { - let query = selectStar() + let query = all().select(\.self) return try query.fetchAll(db) } + @inlinable public func fetchOne(_ db: Database) throws -> From.QueryOutput? { - let query = selectStar() + let query = all().select(\.self) return try query.fetchOne(db) } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SelectStatement where QueryValue == () { + @inlinable public func fetchAll( _ db: Database ) throws -> [(From.QueryOutput, repeat (each J).QueryOutput)] @@ -77,6 +88,7 @@ extension SelectStatement where QueryValue == () { try selectStar().fetchAll(db) } + @inlinable public func fetchOne( _ db: Database ) throws -> (From.QueryOutput, repeat (each J).QueryOutput)? From dad79da8f84d1ba9a99241fb09515d811ebb5454 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 31 Mar 2025 19:48:17 -0700 Subject: [PATCH 067/171] Use 'unscoped' branch from structured queries. --- Examples/CaseStudies/DynamicQuery.swift | 2 +- .../CaseStudies/SwiftDataTemplateDemo.swift | 2 +- Examples/CaseStudies/TransactionDemo.swift | 2 +- Examples/Reminders/ReminderForm.swift | 4 +- Examples/Reminders/ReminderRow.swift | 4 +- Examples/Reminders/RemindersListDetail.swift | 6 +- Examples/Reminders/RemindersLists.swift | 2 +- Examples/Reminders/SearchReminders.swift | 2 +- Examples/Reminders/TagsForm.swift | 4 +- Examples/SyncUpTests/SyncUpFormTests.swift | 2 +- Examples/SyncUps/SyncUpForm.swift | 2 +- Examples/SyncUps/SyncUpsList.swift | 2 +- Package.swift | 4 +- .../xcshareddata/swiftpm/Package.resolved | 10 +-- .../Articles/ComparisonWithSwiftData.md | 2 +- .../Articles/DynamicQueries.md | 2 +- .../Documentation.docc/Articles/Fetching.md | 6 +- .../Documentation.docc/SharingGRDB.md | 4 +- Sources/SharingGRDB/FetchKey.swift | 4 +- Tests/SharingGRDBTests/IntegrationTests.swift | 83 ++++++++++--------- 20 files changed, 76 insertions(+), 73 deletions(-) diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index 83a6aa5a..64dd0aac 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -95,7 +95,7 @@ struct DynamicQueryDemo: SwiftUICaseStudy { return try Value( facts: search.fetchAll(db), searchCount: search.fetchCount(db), - totalCount: Fact.all().fetchCount(db) + totalCount: Fact.all.fetchCount(db) ) } } diff --git a/Examples/CaseStudies/SwiftDataTemplateDemo.swift b/Examples/CaseStudies/SwiftDataTemplateDemo.swift index 1ab1918b..3094efd9 100644 --- a/Examples/CaseStudies/SwiftDataTemplateDemo.swift +++ b/Examples/CaseStudies/SwiftDataTemplateDemo.swift @@ -10,7 +10,7 @@ struct SwiftDataTemplateView: SwiftUICaseStudy { let caseStudyTitle = "SwiftData Template" @Dependency(\.defaultDatabase) private var database - @SharedReader(.fetchAll(Item.all(), animation: .default)) private var items + @SharedReader(.fetchAll(Item.all, animation: .default)) private var items var body: some View { NavigationStack { diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index 1e946563..56589306 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -62,7 +62,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.all.fetchCount(db) ) } } diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 30ee5eaf..7ea9e5ac 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -135,7 +135,7 @@ struct ReminderFormView: View { do { selectedTags = try await database.read { db in try Tag.order(by: \.name) - .join(ReminderTag.all()) { $0.id.eq($1.tagID) && $1.reminderID.eq(reminderID) } + .join(ReminderTag.all) { $0.id.eq($1.tagID) && $1.reminderID.eq(reminderID) } .select { tag, _ in tag } .fetchAll(db) } @@ -211,7 +211,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.all.fetchOne(db)! return ( remindersList, try Reminder.where { $0.remindersListID == remindersList.id }.fetchOne(db)! diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 83247bea..ae687589 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -133,8 +133,8 @@ struct ReminderRowPreview: PreviewProvider { let _ = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() try $0.defaultDatabase.read { db in - reminder = try Reminder.all().fetchOne(db) - reminderList = try RemindersList.all().fetchOne(db)! + reminder = try Reminder.all.fetchOne(db) + reminderList = try RemindersList.all.fetchOne(db)! } } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 4dd0fc1d..3b49c991 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -145,8 +145,8 @@ struct RemindersListDetailView: View { extension Reminder { static let withTags = group(by: \.id) - .leftJoin(ReminderTag.all()) { $0.id.eq($1.reminderID) } - .leftJoin(Tag.all()) { $1.tagID.eq($2.id) } + .leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) } + .leftJoin(Tag.all) { $1.tagID.eq($2.id) } } struct RemindersListDetailPreview: PreviewProvider { @@ -154,7 +154,7 @@ struct RemindersListDetailPreview: PreviewProvider { let remindersList = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() return try $0.defaultDatabase.read { db in - try RemindersList.all().fetchOne(db)! + try RemindersList.all.fetchOne(db)! } } NavigationStack { diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 79e0146b..812fe8b6 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -26,7 +26,7 @@ struct RemindersListsView: View { .fetchAll( RemindersList .group(by: \.id) - .leftJoin(Reminder.all()) { $0.id.eq($1.remindersListID) } + .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) } .select { ReminderListState.Columns( reminderCount: #sql("count(iif(\($1.isCompleted), NULL, \($1.id)))"), diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 4bf3e6e9..d78c1731 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -81,7 +81,7 @@ struct SearchRemindersView: View { .where { showCompletedInSearchResults || !$0.isCompleted } .order { ($0.isCompleted, $0.date) } .withTags - .join(RemindersList.all()) { $0.remindersListID.eq($3.id) } + .join(RemindersList.all) { $0.remindersListID.eq($3.id) } .select { ReminderState.Columns( commaSeparatedTags: $2.name.groupConcat(), diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index 092c7278..a35b6fb9 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -47,8 +47,8 @@ struct TagsView: View { func fetch(_ db: Database) throws -> Value { let top = try Tag .group(by: \.id) - .join(ReminderTag.all()) { $0.id.eq($1.tagID)} - .join(Reminder.all()) { $1.reminderID.eq($2.id)} + .join(ReminderTag.all) { $0.id.eq($1.tagID)} + .join(Reminder.all) { $1.reminderID.eq($2.id)} .having { $2.count().gt(0) } .order { ($2.count().desc(), $0.name) } .limit(3) diff --git a/Examples/SyncUpTests/SyncUpFormTests.swift b/Examples/SyncUpTests/SyncUpFormTests.swift index fe55c3a2..aab71de9 100644 --- a/Examples/SyncUpTests/SyncUpFormTests.swift +++ b/Examples/SyncUpTests/SyncUpFormTests.swift @@ -37,7 +37,7 @@ struct SyncUpFormTests { $0.uuid = .incrementing } let existingSyncUp = try await database.read { db in - try #require(try SyncUp.all().fetchOne(db)) + try #require(try SyncUp.all.fetchOne(db)) } let draft = SyncUp.Draft(existingSyncUp) let model = SyncUpFormModel(syncUp: draft) diff --git a/Examples/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUpForm.swift index c7458b24..8a9f64dc 100644 --- a/Examples/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUpForm.swift @@ -39,7 +39,7 @@ final class SyncUpFormModel: Identifiable { withErrorReporting { self.attendees = try database.read { db in - try Attendee.all() + try Attendee.all .where { $0.syncUpID.eq(syncUpID) } .fetchAll(db) .map { (attendee: Attendee) in AttendeeDraft(id: uuid(), name: attendee.name) } diff --git a/Examples/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUpsList.swift index eef7cb64..952af551 100644 --- a/Examples/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUpsList.swift @@ -11,7 +11,7 @@ final class SyncUpsListModel { .fetchAll( SyncUp .group(by: \.id) - .leftJoin(Attendee.all()) { $0.id.eq($1.syncUpID) } + .leftJoin(Attendee.all) { $0.id.eq($1.syncUpID) } .select { Record.Columns(attendeeCount: $1.count(), syncUp: $0) }, animation: .default ) diff --git a/Package.swift b/Package.swift index a194e990..80eea5f7 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), + .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "unscoped"), ], targets: [ .target( @@ -45,6 +45,7 @@ let package = Package( dependencies: [ "SharingGRDB", .product(name: "DependenciesTestSupport", package: "swift-dependencies"), + .product(name: "StructuredQueries", package: "swift-structured-queries"), ] ), @@ -68,6 +69,7 @@ let package = Package( dependencies: [ "StructuredQueriesGRDB", .product(name: "DependenciesTestSupport", package: "swift-dependencies"), + .product(name: "StructuredQueries", package: "swift-structured-queries"), ] ) ], diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1c03d192..23a3115b 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "7939737e84a296cb06977fbf69313478a9a8e3276c896f8a388f1eba840eeb84", + "originHash" : "64f6ac7f25940871c359a06f3e0d1c66e046838347b60b510af61a72761609a3", "pins" : [ { "identity" : "combine-schedulers", @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "ec2862d1364536fc22ec56a3094e7a034bbc7da8", - "version" : "1.8.1" + "revision" : "4e6b6a814675daf2c1973514314283448f95f941", + "version" : "1.9.0" } }, { @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "branch" : "main", - "revision" : "7db4780ab7f5bd9c434623512fadabb9df413788" + "branch" : "unscoped", + "revision" : "22161de41d23b0ca5bb8fb4579b021b3e29568d9" } }, { diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md index be312017..f623ce56 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -418,7 +418,7 @@ struct SportWithTeamCount { .fetchAll( Sport .group(by: \.id) - .join(Team.all()) { $0.id.eq($1.sportID) } + .join(Team.all { $0.id.eq($1.sportID) } .select { SportWithTeamCount.Columns(sport: $0, teamCount: $1.count()) } diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md b/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md index 0ddf294f..d846cb61 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md @@ -17,7 +17,7 @@ Take the following example: ```swift struct ContentView: View { - @SharedReader(.fetchAll(Item.all()) var items + @SharedReader(.fetchAll(Item.all) var items @State var filterDate: Date? @State var order: SortOrder = .reverse diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md index 98e2ad15..9aee6493 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md @@ -26,7 +26,7 @@ from a table, you can do so by plugging the query directly into @Comment { TODO: Add '@Table' definition? } ```swift -@SharedReader(.fetchAll(Item.all()) +@SharedReader(.fetchAll(Item.all) var items ``` @@ -176,7 +176,7 @@ which means if you have existing code exercising GRDB APIs, they are immediately example, you may have some code that is using GRDB's built-in query builder: ```swift -let query = Item.all() +let query = Item.all .filter(!Column("isInStock")) .order(Column("createdAt").desc) ``` @@ -187,7 +187,7 @@ you are free to use it however you please: ```swift struct Items: FetchKeyRequest { func fetch(_ db: Database) throws -> [Item] { - try Item.all() + try Item.all .filter(!Column("isInStock")) .order(Column("createdAt").desc) .fetch(db) diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index 5279e52f..485d81d9 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -9,7 +9,7 @@ back to the iOS 13 generation of targets. @Column { ```swift // SharingGRDB - @SharedReader(.fetch(Item.all()) + @SharedReader(.fetch(Item.all)) var items ``` } @@ -81,7 +81,7 @@ This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies [`fetchAll`](): ```swift -@SharedReader(.fetch(Item.all())) +@SharedReader(.fetch(Item.all)) var items ``` diff --git a/Sources/SharingGRDB/FetchKey.swift b/Sources/SharingGRDB/FetchKey.swift index 2bbcc6b0..8e4db446 100644 --- a/Sources/SharingGRDB/FetchKey.swift +++ b/Sources/SharingGRDB/FetchKey.swift @@ -17,8 +17,8 @@ extension SharedReaderKey { /// ```swift /// struct Items: FetchKeyRequest { /// func fetch(_ db: Database) throws -> [Item] { - /// try Item.all() - /// .order(Column("timestamp").desc) + /// try Item.all + /// .order { $0.timestamp.desc() } /// .fetchAll(db) /// } /// } diff --git a/Tests/SharingGRDBTests/IntegrationTests.swift b/Tests/SharingGRDBTests/IntegrationTests.swift index 9159b138..80c5655f 100644 --- a/Tests/SharingGRDBTests/IntegrationTests.swift +++ b/Tests/SharingGRDBTests/IntegrationTests.swift @@ -1,34 +1,35 @@ import Dependencies import DependenciesTestSupport -import GRDB import Sharing import SharingGRDB +import StructuredQueries import Testing @Suite(.dependency(\.defaultDatabase, try .syncUps())) struct IntegrationTests { + @Dependency(\.defaultDatabase) var database + @Test func fetchAll_SQLString() async throws { - @SharedReader(.fetchAll(sql: #"SELECT * FROM "syncUps" WHERE "isActive""#)) + @SharedReader(.fetchAll(SyncUp.where(\.isActive))) var syncUps: [SyncUp] = [] #expect(syncUps == []) - @Dependency(\.defaultDatabase) var database try await database.write { db in - _ = try SyncUp(isActive: true, title: "Engineering") - .inserted(db) + _ = try SyncUp.insert(SyncUp.Draft(isActive: true, title: "Engineering")) + .execute(db) } try await Task.sleep(nanoseconds: 10_000_000) #expect(syncUps == [SyncUp(id: 1, isActive: true, title: "Engineering")]) try await database.write { db in - _ = try SyncUp(id: 1, isActive: false, title: "Engineering") - .saved(db) + _ = try SyncUp.upsert(SyncUp.Draft(id: 1, isActive: false, title: "Engineering")) + .execute(db) } try await Task.sleep(nanoseconds: 10_000_000) #expect(syncUps == []) try await database.write { db in - _ = try SyncUp(id: 1, isActive: true, title: "Engineering") - .saved(db) + _ = try SyncUp.upsert(SyncUp.Draft(id: 1, isActive: true, title: "Engineering")) + .execute(db) } try await Task.sleep(nanoseconds: 10_000_000) #expect(syncUps == [SyncUp(id: 1, isActive: true, title: "Engineering")]) @@ -40,46 +41,39 @@ struct IntegrationTests { var syncUps: [SyncUp] = [] #expect(syncUps == []) - @Dependency(\.defaultDatabase) var database try await database.write { db in - _ = try SyncUp(isActive: true, title: "Engineering") - .inserted(db) + _ = try SyncUp.insert(SyncUp.Draft(isActive: true, title: "Engineering")) + .execute(db) } try await Task.sleep(nanoseconds: 10_000_000) #expect(syncUps == [SyncUp(id: 1, isActive: true, title: "Engineering")]) try await database.write { db in - _ = try SyncUp(id: 1, isActive: false, title: "Engineering") - .saved(db) + _ = try SyncUp.upsert(SyncUp.Draft(id: 1, isActive: false, title: "Engineering")) + .execute(db) } try await Task.sleep(nanoseconds: 10_000_000) #expect(syncUps == []) try await database.write { db in - _ = try SyncUp(id: 1, isActive: true, title: "Engineering") - .saved(db) + _ = try SyncUp.upsert(SyncUp.Draft(id: 1, isActive: true, title: "Engineering")) + .execute(db) } try await Task.sleep(nanoseconds: 10_000_000) #expect(syncUps == [SyncUp(id: 1, isActive: true, title: "Engineering")]) } } -private struct SyncUp: Codable, Equatable, FetchableRecord, MutablePersistableRecord { - var id: Int64? +@Table +private struct SyncUp: Equatable, Identifiable { + let id: Int var isActive: Bool var title: String - static let databaseTableName = "syncUps" - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } } -private struct Attendee: Codable, Equatable, FetchableRecord, MutablePersistableRecord { - var id: Int64? +@Table +private struct Attendee: Equatable { + let id: Int var name: String - var syncUpID: Int - static let databaseTableName = "attendees" - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } + var syncUpID: SyncUp.ID } private extension DatabaseWriter where Self == DatabaseQueue { @@ -87,16 +81,24 @@ private extension DatabaseWriter where Self == DatabaseQueue { let database = try DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create schema") { db in - try db.create(table: SyncUp.databaseTableName) { t in - t.autoIncrementedPrimaryKey("id") - t.column("isActive", .boolean).notNull() - t.column("title", .text).notNull() - } - try db.create(table: Attendee.databaseTableName) { t in - t.autoIncrementedPrimaryKey("id") - t.column("syncUpID", .integer).notNull() - t.column("name", .text).notNull() - } + try #sql(""" + CREATE TABLE "syncUps" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "isActive" INTEGER NOT NULL, + "title" TEXT NOT NULL + ) + """) + .execute(db) + try #sql(""" + CREATE TABLE "attendees" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "syncUpID" INTEGER NOT NULL, + "name" TEXT NOT NULL, + + FOREIGN KEY("syncUpID") REFERENCES "syncUps"("id") + ) + """) + .execute(db) } try migrator.migrate(database) return database @@ -106,8 +108,7 @@ private extension DatabaseWriter where Self == DatabaseQueue { private struct ActiveSyncUps: FetchKeyRequest { func fetch(_ db: Database) throws -> [SyncUp] { try SyncUp - .all() - .filter(Column("isActive")) + .where(\.isActive) .fetchAll(db) } } From 4bbc4318903686be2bd52fb05c9fed4ff6b9ea7c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 1 Apr 2025 18:22:37 -0700 Subject: [PATCH 068/171] Update for unscoped branch. --- Package.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 15 ++++++++++++--- Sources/SharingGRDB/FetchKeyRequest.swift | 6 +++--- .../Statement+GRDB.swift | 6 +++--- .../MigrationTests.swift | 2 +- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index 80eea5f7..8529eadb 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "unscoped"), + .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), ], targets: [ .target( diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 23a3115b..85ed17ac 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "64f6ac7f25940871c359a06f3e0d1c66e046838347b60b510af61a72761609a3", + "originHash" : "141d3aed2910d045d6dc4900a02d3b45ca6b03d07df4afef17680976fc0c5a57", "pins" : [ { "identity" : "combine-schedulers", @@ -127,13 +127,22 @@ "version" : "2.3.3" } }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "1be8144023c367c5de701a6313ed29a3a10bf59b", + "version" : "1.18.3" + } + }, { "identity" : "swift-structured-queries", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "branch" : "unscoped", - "revision" : "22161de41d23b0ca5bb8fb4579b021b3e29568d9" + "branch" : "main", + "revision" : "7999fe596aa661f8aec59e5b0f3e5601026831f9" } }, { diff --git a/Sources/SharingGRDB/FetchKeyRequest.swift b/Sources/SharingGRDB/FetchKeyRequest.swift index 14d9d002..f9fe3ce8 100644 --- a/Sources/SharingGRDB/FetchKeyRequest.swift +++ b/Sources/SharingGRDB/FetchKeyRequest.swift @@ -7,9 +7,9 @@ import GRDB /// ```swift /// struct Players: FetchKeyRequest { /// func fetch(_ db: Database) throws -> [Player] { -/// try Player.all() -/// .filter(Column("isInjured") == false) -/// .order(Column("name")) +/// try Player +/// .where { !$0.isInjured } +/// .order(by: \.name) /// .limit(10) /// .fetchAll(db) /// } diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index fbe1c7f9..7efa94db 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -59,7 +59,7 @@ extension StructuredQueriesCore.Statement { extension SelectStatement where QueryValue == (), Joins == () { @inlinable public func fetchCount(_ db: Database) throws -> Int { - let query = all().count() + let query = all.count() return try query.fetchOne(db) ?? 0 } } @@ -67,13 +67,13 @@ extension SelectStatement where QueryValue == (), Joins == () { extension SelectStatement where QueryValue == (), Joins == () { @inlinable public func fetchAll(_ db: Database) throws -> [From.QueryOutput] { - let query = all().select(\.self) + let query = all.select(\.self) return try query.fetchAll(db) } @inlinable public func fetchOne(_ db: Database) throws -> From.QueryOutput? { - let query = all().select(\.self) + let query = all.select(\.self) return try query.fetchOne(db) } } diff --git a/Tests/StructuredQueriesGRDBTests/MigrationTests.swift b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift index 396fa4a1..9af43227 100644 --- a/Tests/StructuredQueriesGRDBTests/MigrationTests.swift +++ b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift @@ -24,7 +24,7 @@ import Testing let grdbDate = try Date.fetchOne(db, sql: "SELECT * FROM models") try #expect(abs(#require(grdbDate).timeIntervalSince1970 - timestamp) < 0.001) - let date = try #require(try Model.all().fetchOne(db)).date + let date = try #require(try Model.all.fetchOne(db)).date try #expect(abs(#require(date).timeIntervalSince1970 - timestamp) < 0.001) } } From 5d38f098ab4747650ec9dd9780cd441a5ff8cf75 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 1 Apr 2025 18:26:04 -0700 Subject: [PATCH 069/171] clean up --- .../MigrationTests.swift | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Tests/StructuredQueriesGRDBTests/MigrationTests.swift b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift index 9af43227..2c487af3 100644 --- a/Tests/StructuredQueriesGRDBTests/MigrationTests.swift +++ b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift @@ -4,15 +4,17 @@ import StructuredQueriesGRDB import Testing @Suite struct MigrationTests { + @available(iOS 15, *) @Test func dates() throws { let database = try DatabaseQueue() - var migrator = DatabaseMigrator() - migrator.registerMigration("Create schema") { db in - try db.create(table: "models") { t in - t.column("date", .datetime).notNull() - } + try database.write { db in + try #sql(""" + CREATE TABLE "models" ( + "date" TEXT NOT NULL + ) + """) + .execute(db) } - try migrator.migrate(database) let timestamp = 123.456 try database.write { db in @@ -30,7 +32,8 @@ import Testing } } +@available(iOS 15, *) @Table private struct Model { - @Column(as: .iso8601) + @Column(as: Date.ISO8601Representation.self) var date: Date } From 079bb1fefbd58258ff30f966c38c4e7c1abb5b41 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 3 Apr 2025 17:14:41 -0700 Subject: [PATCH 070/171] wip --- Examples/Reminders/ReminderForm.swift | 4 ++-- Examples/Reminders/ReminderRow.swift | 4 ++-- Examples/Reminders/RemindersListDetail.swift | 6 ++--- Examples/Reminders/RemindersLists.swift | 2 +- Examples/Reminders/SearchReminders.swift | 2 +- Examples/Reminders/TagsForm.swift | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 23 +++++++++++++------ .../Statement+GRDB.swift | 20 +++++++++++++--- 8 files changed, 44 insertions(+), 21 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 30ee5eaf..7ea9e5ac 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -135,7 +135,7 @@ struct ReminderFormView: View { do { selectedTags = try await database.read { db in try Tag.order(by: \.name) - .join(ReminderTag.all()) { $0.id.eq($1.tagID) && $1.reminderID.eq(reminderID) } + .join(ReminderTag.all) { $0.id.eq($1.tagID) && $1.reminderID.eq(reminderID) } .select { tag, _ in tag } .fetchAll(db) } @@ -211,7 +211,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.all.fetchOne(db)! return ( remindersList, try Reminder.where { $0.remindersListID == remindersList.id }.fetchOne(db)! diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 83247bea..ae687589 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -133,8 +133,8 @@ struct ReminderRowPreview: PreviewProvider { let _ = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() try $0.defaultDatabase.read { db in - reminder = try Reminder.all().fetchOne(db) - reminderList = try RemindersList.all().fetchOne(db)! + reminder = try Reminder.all.fetchOne(db) + reminderList = try RemindersList.all.fetchOne(db)! } } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 4dd0fc1d..3b49c991 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -145,8 +145,8 @@ struct RemindersListDetailView: View { extension Reminder { static let withTags = group(by: \.id) - .leftJoin(ReminderTag.all()) { $0.id.eq($1.reminderID) } - .leftJoin(Tag.all()) { $1.tagID.eq($2.id) } + .leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) } + .leftJoin(Tag.all) { $1.tagID.eq($2.id) } } struct RemindersListDetailPreview: PreviewProvider { @@ -154,7 +154,7 @@ struct RemindersListDetailPreview: PreviewProvider { let remindersList = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() return try $0.defaultDatabase.read { db in - try RemindersList.all().fetchOne(db)! + try RemindersList.all.fetchOne(db)! } } NavigationStack { diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 79e0146b..812fe8b6 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -26,7 +26,7 @@ struct RemindersListsView: View { .fetchAll( RemindersList .group(by: \.id) - .leftJoin(Reminder.all()) { $0.id.eq($1.remindersListID) } + .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) } .select { ReminderListState.Columns( reminderCount: #sql("count(iif(\($1.isCompleted), NULL, \($1.id)))"), diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 4bf3e6e9..d78c1731 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -81,7 +81,7 @@ struct SearchRemindersView: View { .where { showCompletedInSearchResults || !$0.isCompleted } .order { ($0.isCompleted, $0.date) } .withTags - .join(RemindersList.all()) { $0.remindersListID.eq($3.id) } + .join(RemindersList.all) { $0.remindersListID.eq($3.id) } .select { ReminderState.Columns( commaSeparatedTags: $2.name.groupConcat(), diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index 092c7278..a35b6fb9 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -47,8 +47,8 @@ struct TagsView: View { func fetch(_ db: Database) throws -> Value { let top = try Tag .group(by: \.id) - .join(ReminderTag.all()) { $0.id.eq($1.tagID)} - .join(Reminder.all()) { $1.reminderID.eq($2.id)} + .join(ReminderTag.all) { $0.id.eq($1.tagID)} + .join(Reminder.all) { $1.reminderID.eq($2.id)} .having { $2.count().gt(0) } .order { ($2.count().desc(), $0.name) } .limit(3) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1c03d192..bbac3df6 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "52b7b7bd26821c67f1c26b5e248fd6bcac4903a7", - "version" : "7.4.0" + "revision" : "04e73c26c4ce8218ab85aaf791942bb0b204f330", + "version" : "7.4.1" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "ec2862d1364536fc22ec56a3094e7a034bbc7da8", - "version" : "1.8.1" + "revision" : "4e6b6a814675daf2c1973514314283448f95f941", + "version" : "1.9.0" } }, { @@ -123,8 +123,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-sharing", "state" : { - "revision" : "2c840cf2ae0526ad6090e7796c4e13d9a2339f4a", - "version" : "2.3.3" + "revision" : "732871fabfc6b38fcdff5ad2f7336327dbf78e81", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "1be8144023c367c5de701a6313ed29a3a10bf59b", + "version" : "1.18.3" } }, { @@ -133,7 +142,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "7db4780ab7f5bd9c434623512fadabb9df413788" + "revision" : "f3be457384b9175e670fafe6bdb75795ac47b167" } }, { diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index fbe1c7f9..11a4aad4 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -59,7 +59,7 @@ extension StructuredQueriesCore.Statement { extension SelectStatement where QueryValue == (), Joins == () { @inlinable public func fetchCount(_ db: Database) throws -> Int { - let query = all().count() + let query = all.count() return try query.fetchOne(db) ?? 0 } } @@ -67,15 +67,21 @@ extension SelectStatement where QueryValue == (), Joins == () { extension SelectStatement where QueryValue == (), Joins == () { @inlinable public func fetchAll(_ db: Database) throws -> [From.QueryOutput] { - let query = all().select(\.self) + let query = all.select(\.self) return try query.fetchAll(db) } @inlinable public func fetchOne(_ db: Database) throws -> From.QueryOutput? { - let query = all().select(\.self) + let query = all.select(\.self) return try query.fetchOne(db) } + + @inlinable + public func fetchCursor(_ db: Database) throws -> QueryCursor { + let query = selectStar() + return try query.fetchCursor(db) + } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @@ -95,4 +101,12 @@ extension SelectStatement where QueryValue == () { where Joins == (repeat each J) { try selectStar().fetchOne(db) } + + @inlinable + public func fetchCursor( + _ db: Database + ) throws -> QueryCursor<(From.QueryOutput, repeat (each J).QueryOutput)> + where Joins == (repeat each J) { + try selectStar().fetchCursor(db) + } } From 6156e1059f38b7b903607542f0f75325979fd52d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 4 Apr 2025 22:12:13 -0700 Subject: [PATCH 071/171] fix --- Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index 11a4aad4..10312f58 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -59,7 +59,7 @@ extension StructuredQueriesCore.Statement { extension SelectStatement where QueryValue == (), Joins == () { @inlinable public func fetchCount(_ db: Database) throws -> Int { - let query = all.count() + let query = asSelect().count() return try query.fetchOne(db) ?? 0 } } @@ -67,13 +67,13 @@ extension SelectStatement where QueryValue == (), Joins == () { extension SelectStatement where QueryValue == (), Joins == () { @inlinable public func fetchAll(_ db: Database) throws -> [From.QueryOutput] { - let query = all.select(\.self) + let query = asSelect().select(\.self) return try query.fetchAll(db) } @inlinable public func fetchOne(_ db: Database) throws -> From.QueryOutput? { - let query = all.select(\.self) + let query = asSelect().select(\.self) return try query.fetchOne(db) } From 945b996f68f85672549f91bf1e4c84bffb5f54c4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 4 Apr 2025 22:38:21 -0700 Subject: [PATCH 072/171] wip --- Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index 10312f58..7a8733da 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -67,20 +67,17 @@ extension SelectStatement where QueryValue == (), Joins == () { extension SelectStatement where QueryValue == (), Joins == () { @inlinable public func fetchAll(_ db: Database) throws -> [From.QueryOutput] { - let query = asSelect().select(\.self) - return try query.fetchAll(db) + try Array(fetchCursor(db)) } @inlinable public func fetchOne(_ db: Database) throws -> From.QueryOutput? { - let query = asSelect().select(\.self) - return try query.fetchOne(db) + try fetchCursor(db).next() } @inlinable public func fetchCursor(_ db: Database) throws -> QueryCursor { - let query = selectStar() - return try query.fetchCursor(db) + try QueryValueCursor(db: db, query: query) } } From a124b32caee6524a3cc2914eefa27344c884b1f0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 4 Apr 2025 22:39:41 -0700 Subject: [PATCH 073/171] wip --- Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index 7a8733da..e98041bb 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -88,7 +88,7 @@ extension SelectStatement where QueryValue == () { _ db: Database ) throws -> [(From.QueryOutput, repeat (each J).QueryOutput)] where Joins == (repeat each J) { - try selectStar().fetchAll(db) + try Array(fetchCursor(db)) } @inlinable @@ -96,7 +96,7 @@ extension SelectStatement where QueryValue == () { _ db: Database ) throws -> (From.QueryOutput, repeat (each J).QueryOutput)? where Joins == (repeat each J) { - try selectStar().fetchOne(db) + try fetchCursor(db).next() } @inlinable @@ -104,6 +104,6 @@ extension SelectStatement where QueryValue == () { _ db: Database ) throws -> QueryCursor<(From.QueryOutput, repeat (each J).QueryOutput)> where Joins == (repeat each J) { - try selectStar().fetchCursor(db) + try QueryPackCursor(db: db, query: query) } } From 0140ec218a03ef914bae041f796926bd09d29632 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 4 Apr 2025 23:43:18 -0700 Subject: [PATCH 074/171] wip --- Sources/StructuredQueriesGRDBCore/QueryCursor.swift | 2 +- Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index 4427b8ab..011b0f99 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -30,7 +30,7 @@ final class QueryValueCursor: QueryCursor Element { - let element = try decoder.decodeColumns(QueryValue.self) + let element = try QueryValue(decoder: &decoder).queryOutput decoder.next() return element } diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index e98041bb..25b58fcc 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -11,7 +11,10 @@ extension StructuredQueriesCore.Statement { @inlinable public func fetchAll(_ db: Database) throws -> [QueryValue.QueryOutput] where QueryValue: QueryRepresentable { - try Array(fetchCursor(db)) + let cursor = try QueryValueCursor(db: db, query: query) + var output: [QueryValue.QueryOutput] = [] + try cursor.forEach { output.append($0) } + return output } @inlinable @@ -67,7 +70,10 @@ extension SelectStatement where QueryValue == (), Joins == () { extension SelectStatement where QueryValue == (), Joins == () { @inlinable public func fetchAll(_ db: Database) throws -> [From.QueryOutput] { - try Array(fetchCursor(db)) + let cursor = try QueryValueCursor(db: db, query: query) + var output: [From.QueryOutput] = [] + try cursor.forEach { output.append($0) } + return output } @inlinable From ef484c6e217c7b06282ef44a93ec94452972b2e3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 6 Apr 2025 11:35:51 -0700 Subject: [PATCH 075/171] update migratinos --- Examples/CaseStudies/Animations.swift | 11 ++++--- Examples/CaseStudies/DynamicQuery.swift | 11 ++++--- .../CaseStudies/ObservableModelDemo.swift | 11 ++++--- .../CaseStudies/SwiftDataTemplateDemo.swift | 11 ++++--- Examples/CaseStudies/SwiftUIDemo.swift | 11 ++++--- Examples/CaseStudies/TransactionDemo.swift | 11 ++++--- Examples/CaseStudies/UIKitDemo.swift | 11 ++++--- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Articles/ComparisonWithSwiftData.md | 29 +++++++++++-------- Tests/SharingGRDBTests/SharingGRDBTests.swift | 16 ++++++---- 10 files changed, 77 insertions(+), 47 deletions(-) diff --git a/Examples/CaseStudies/Animations.swift b/Examples/CaseStudies/Animations.swift index f8b61547..9692c81f 100644 --- a/Examples/CaseStudies/Animations.swift +++ b/Examples/CaseStudies/Animations.swift @@ -61,10 +61,13 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("body", .text).notNull() - } + try #sql(""" + CREATE TABLE "facts" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "body" TEXT NOT NULL + ) + """) + .execute(db) } try! migrator.migrate(databaseQueue) return databaseQueue diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index 64dd0aac..6db68708 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -112,10 +112,13 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("body", .text).notNull() - } + try #sql(""" + CREATE TABLE "facts" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "body" TEXT NOT NULL + ) + """) + .execute(db) } try! migrator.migrate(databaseQueue) return databaseQueue diff --git a/Examples/CaseStudies/ObservableModelDemo.swift b/Examples/CaseStudies/ObservableModelDemo.swift index 8bacbeef..99edaad9 100644 --- a/Examples/CaseStudies/ObservableModelDemo.swift +++ b/Examples/CaseStudies/ObservableModelDemo.swift @@ -102,10 +102,13 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("body", .text).notNull() - } + try #sql(""" + CREATE TABLE "facts" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "body" TEXT NOT NULL + ) + """) + .execute(db) } try! migrator.migrate(databaseQueue) return databaseQueue diff --git a/Examples/CaseStudies/SwiftDataTemplateDemo.swift b/Examples/CaseStudies/SwiftDataTemplateDemo.swift index 3094efd9..d6824541 100644 --- a/Examples/CaseStudies/SwiftDataTemplateDemo.swift +++ b/Examples/CaseStudies/SwiftDataTemplateDemo.swift @@ -68,10 +68,13 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create items table") { db in - try db.create(table: Item.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("timestamp", .datetime).notNull().defaults(sql: "CURRENT_TIMESTAMP") - } + try #sql(""" + CREATE TABLE "items" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "timestamp" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """) + .execute(db) } try! migrator.migrate(databaseQueue) return databaseQueue diff --git a/Examples/CaseStudies/SwiftUIDemo.swift b/Examples/CaseStudies/SwiftUIDemo.swift index a76c343c..eef801e7 100644 --- a/Examples/CaseStudies/SwiftUIDemo.swift +++ b/Examples/CaseStudies/SwiftUIDemo.swift @@ -65,10 +65,13 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("body", .text).notNull() - } + try #sql(""" + CREATE TABLE "facts" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "body" TEXT NOT NULL + ) + """) + .execute(db) } try! migrator.migrate(databaseQueue) return databaseQueue diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index 56589306..069ff5dd 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -80,10 +80,13 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("body", .text).notNull() - } + try #sql(""" + CREATE TABLE "facts" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "body" TEXT NOT NULL + ) + """) + .execute(db) } try! migrator.migrate(databaseQueue) return databaseQueue diff --git a/Examples/CaseStudies/UIKitDemo.swift b/Examples/CaseStudies/UIKitDemo.swift index 332f4db1..0378e101 100644 --- a/Examples/CaseStudies/UIKitDemo.swift +++ b/Examples/CaseStudies/UIKitDemo.swift @@ -126,10 +126,13 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try db.create(table: Fact.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("body", .text).notNull() - } + try #sql(""" + CREATE TABLE "facts" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "body" TEXT NOT NULL + ) + """) + .execute(db) } try! migrator.migrate(databaseQueue) return databaseQueue diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 774c6d9e..5ecbd6ce 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -142,7 +142,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "f3be457384b9175e670fafe6bdb75795ac47b167" + "revision" : "5e33730401f1ba2d2ed98e8d349d17d9480c4942" } }, { diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md index f623ce56..02888a45 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -459,11 +459,14 @@ Lightweight migrations in SwiftData work for simple situations, such as adding a } migrator.registerMigration("Create 'items' table") { db in - db.createTable(Item.tableName) { table in - table.autoIncrementedPrimaryKey("id") - table.column("title", .text).notNull() - table.column("isInStock", .boolean).notNull().defaults(to: true) - } + try #sql(""" + CREATE TABLE "items" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL, + "isInStock" INTEGER NOT NULL DEFAULT 1 + ) + """) + .execute(db) } ``` } @@ -497,9 +500,10 @@ adding a `description` field to the `Item` type: } migrator.registerMigration("Add 'description' column to 'items'") { db in - db.alterTable(Item.tableName) { table in - table.add(column: "description", .text) - } + try #sql(""" + ALTER TABLE "items" ADD COLUMN "description" TEXT + """) + .execute(db) } ``` } @@ -567,14 +571,15 @@ structure of your data types. The overall steps to follow are as such: try Item .where { !$0.id.in( - Item - .select { $0.id.min() } - .group(by: \.title) + Item.select { $0.id.min() }.group(by: \.title) ) } .execute() // 2️⃣ Create unique index - try db.create(indexOn: "items", columns: ["title"], options: .unique) + try #sql(""" + CREATE UNIQUE INDEX "items_title" ON "items" ("title") + """) + .execute(db) } ``` } diff --git a/Tests/SharingGRDBTests/SharingGRDBTests.swift b/Tests/SharingGRDBTests/SharingGRDBTests.swift index 5ee839c7..b4105eee 100644 --- a/Tests/SharingGRDBTests/SharingGRDBTests.swift +++ b/Tests/SharingGRDBTests/SharingGRDBTests.swift @@ -2,6 +2,7 @@ import Dependencies import GRDB import Sharing import SharingGRDB +import StructuredQueries import Testing @Suite struct GRDBSharingTests { @@ -83,16 +84,16 @@ import Testing } } -fileprivate struct Fetch1: FetchKeyRequest { +private struct Fetch1: FetchKeyRequest { func fetch(_ db: Database) throws { } } -fileprivate struct Fetch2: FetchKeyRequest { +private struct Fetch2: FetchKeyRequest { func fetch(_ db: Database) throws { } } -fileprivate struct Record: Codable, Equatable, FetchableRecord, MutablePersistableRecord { +private struct Record: Codable, Equatable, FetchableRecord, MutablePersistableRecord { static let databaseTableName = "records" let id: Int } @@ -102,9 +103,12 @@ extension DatabaseWriter where Self == DatabaseQueue { let database = try DatabaseQueue(named: "db") var migrator = DatabaseMigrator() migrator.registerMigration("Up") { db in - try db.create(table: "records") { table in - table.column("id", .integer).primaryKey(autoincrement: true) - } + try #sql( + """ + CREATE TABLE "records" ("id" INTEGER PRIMARY KEY AUTOINCREMENT) + """ + ) + .execute(db) for index in 1...3 { _ = try Record(id: index).inserted(db) } From 16444333dd2779f8b12191aa0da6a9d2c808d695 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 7 Apr 2025 11:48:16 -0700 Subject: [PATCH 076/171] wip --- README.md | 11 +++-- .../Articles/ComparisonWithSwiftData.md | 25 ++++++----- .../Articles/DynamicQueries.md | 2 +- .../Documentation.docc/Articles/Fetching.md | 41 +++---------------- .../Documentation.docc/Articles/Observing.md | 6 +-- .../Documentation.docc/SharingGRDB.md | 13 +++--- .../QueryCursor.swift | 2 +- .../SQLiteQueryDecoder.swift | 6 +-- 8 files changed, 34 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 49534a3b..f9dd76de 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,7 @@ observed by SwiftUI so that views are recomputed when the external data changes, powered directly by SQLite using [Sharing][sharing-gh] and [GRDB][grdb], and is usable from UIKit, `@Observable` models, and more. -> Note: It is not required to write queries as a raw SQL string, and a query builder can be used -> instead. For more information on SharingGRDB's querying capabilities, see -[Fetching model data][fetching-article]. +For more information on SharingGRDB's querying capabilities, see [Fetching model data][fetching-article]. ## Quick start @@ -127,7 +125,7 @@ This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies [`fetchAll`][fetchall-docs]: ```swift -@SharedReader(.fetchAll(sql: "SELECT * FROM items")) +@SharedReader(.fetchAll(Item.all)) var items: [Item] ``` @@ -146,9 +144,10 @@ a model context, via a property wrapper: @Dependency(\.defaultDatabase) var database -var newItem = Item(/* ... */) +var newItem = try database.write { db in - try newItem.insert(db) + try Item.insert(Item(/* ... */)) + .execute(db)) } ``` diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md index 02888a45..68c10f69 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -109,8 +109,8 @@ whereas you use the `@Query` macro with SwiftData: The `@SharedReader` property wrapper takes a variety of options, detailed more in , and allows you to write raw SQL queries for fetching and aggregating data from your database. It -is also possibly to construct SQL queries using GRDB's query builder syntax. See -[`fetch`]() for more information. +is also possibly to construct SQL queries using SharingGRDB's query builder syntax. See + [`fetchAll`]() for more information. ### Fetching data for an @Observable model @@ -192,7 +192,7 @@ search for rows in a table: // SharingGRDB struct ItemsView: View { @State var searchText = "" - @SharedReader var items: [Item] + @SharedReader(value: []) var items: [Item] var body: some View { ForEach(items) { item in @@ -290,7 +290,7 @@ For example, to get access to the ``Dependencies/DependencyValues/defaultDatabas } } -Then, to create a new row in a table you use the `write` and `insert` methods from GRDB: +Then, to create a new row in a table you use the `write` and `insert` methods from SharingGRDB: @Row { @Column { @@ -299,9 +299,8 @@ Then, to create a new row in a table you use the `write` and `insert` methods fr @Dependency(\.defaultDatabase) var database try database.write { db in - let newItem = Item(/* ... */) - try Item.insert(newItem).execute(db) - try newItem.insert(db) + try Item.insert(Item(/* ... */)) + .execute(db) } ``` } @@ -317,7 +316,7 @@ Then, to create a new row in a table you use the `write` and `insert` methods fr } } -To update an existing row you can use the `write` and `update` methods from GRDB: +To update an existing row you can use the `write` and `update` methods from SharingGRDB: @Row { @Column { @@ -342,7 +341,7 @@ To update an existing row you can use the `write` and `update` methods from GRDB } } -And to delete an existing row, you can use the `write` and `delete` methods from GRDB: +And to delete an existing row, you can use the `write` and `delete` methods from SharingGRDB: @Row { @Column { @@ -351,7 +350,7 @@ And to delete an existing row, you can use the `write` and `delete` methods from @Dependency(\.defaultDatabase) var database try database.write { db in - try Item.deleted(existingItem).execute(db) + try Item.delete(existingItem).execute(db) } ``` } @@ -368,8 +367,8 @@ And to delete an existing row, you can use the `write` and `delete` methods from ### Associations -The biggest difference between SwiftData and GRDB is that SwiftData provides tools for an -Object Relational Mapping (ORM), whereas GRDB is largely just a nice API for interacting with SQLite +The biggest difference between SwiftData and SharingGRDB is that SwiftData provides tools for an +Object Relational Mapping (ORM), whereas SharingGRDB is largely just a nice API for interacting with SQLite directly. For example, SwiftData allows you to model a `Sport` type that belongs to many `Team`s like @@ -418,7 +417,7 @@ struct SportWithTeamCount { .fetchAll( Sport .group(by: \.id) - .join(Team.all { $0.id.eq($1.sportID) } + .join(Team.all) { $0.id.eq($1.sportID) } .select { SportWithTeamCount.Columns(sport: $0, teamCount: $1.count()) } diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md b/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md index d846cb61..de3f62ff 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md @@ -92,5 +92,5 @@ struct ContentView: View { > locally to this view, we use `@State.SharedReader`, instead, which wraps a `@SharedReader` in > SwiftUI `@State`. -> Note: We are using the ``Sharing/SharedReaderKey/fet`` style of +> Note: We are using the ``Sharing/SharedReaderKey/fetchAll(_:database:)`` style of > querying the database. See for more APIs that can be used. diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md index 9aee6493..30e6ca5e 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md @@ -81,14 +81,12 @@ The choice is up to you for each query or query fragment. To learn more, see the ### Querying with raw SQL -@Comment { - TODO: Call out why these tools exist (to allow one to avoid the Swift Syntax cost of building a - macro) -} +SharingGRDB also comes with a more basic set of tools that work directly with GRDB. The primary reason you +may want to use these tools and not the StructuredQueries tools is that they do not require a macro to use, +and so do not incur the cost of compiling SwiftSyntax. -SharingGRDB also comes with a more basic set of tools that work directly with GRDB. This includes -a [`fetchAll`]() key that takes a raw -SQL string: +There is a version of [`fetchAll`]() key that +takes a raw SQL string: ```swift @SharedReader(.fetchAll(sql: "SELECT * FROM items")) var items: [Item] @@ -105,11 +103,6 @@ These APIs simply feed their data directly to GRDB's equivalent `Database` APIs, up to you to safely bind arguments and avoid SQL injection. If you want to write SQL queries by hand, consider using Structured Queries' `#sql` macro, instead. -> Note: While GRDB comes with its own query builder, its lack of hashable identity means that it is -> not directly compatible with `@SharedReader`. It is for this reason SharingGRDB includes bindings -> to [Structured Queries][structured-queries-gh], a powerful SQL builder library that preserves a -> hashable identity for its queries. - [structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries ### Querying with custom requests @@ -171,30 +164,6 @@ items.itemsCount // 100 > Note: A default must be provided to `@SharedReader` since it is querying for a custom data type > instead of a collection of data. -You can perform any kind of work and return any kind of data from ``FetchKeyRequest/fetch(_:)``, -which means if you have existing code exercising GRDB APIs, they are immediately usable. For -example, you may have some code that is using GRDB's built-in query builder: - -```swift -let query = Item.all - .filter(!Column("isInStock")) - .order(Column("createdAt").desc) -``` - -Well there is no need to rewrite this code. Because `fetch` is handed a GRDB database connection, -you are free to use it however you please: - -```swift -struct Items: FetchKeyRequest { - func fetch(_ db: Database) throws -> [Item] { - try Item.all - .filter(!Column("isInStock")) - .order(Column("createdAt").desc) - .fetch(db) - } -} -``` - Typically the conformances to ``FetchKeyRequest`` can even be made private and nested inside whatever type they are used in, such as SwiftUI view, `@Observable` model, or UIKit view controller. The only time it needs to be made public is if it's shared amongst many features. diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Observing.md b/Sources/SharingGRDB/Documentation.docc/Articles/Observing.md index 31e572d6..e829c62c 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/Observing.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/Observing.md @@ -16,7 +16,7 @@ choose one of the various ways for [querying your database](): ```swift struct ItemsView: View { - @SharedReader(.fetchAll(sql: "SELECT * FROM items")) var items: [Item] + @SharedReader(.fetchAll(Item.all)) var items var body: some View { ForEach(items) { item in Text(item.name) @@ -38,7 +38,7 @@ when the database changes and cause any SwiftUI view using it to re-render: @Observable class ItemsModel { @ObservationIgnored - @SharedReader(.fetchAll(sql: "SELECT * FROM items")) var items: [Item] + @SharedReader(.fetchAll(Item.all)) var items } struct ItemsView: View { var body: some View { @@ -61,7 +61,7 @@ then you can do roughly the following: ```swift class ItemsViewController: UICollectionViewController { - @SharedReader(.fetchAll("SELECT * FROM items")) var items: [Item] + @SharedReader(.fetchAll(Item.all)) var items override func viewDidLoad() { // Set up data source and cell registration... diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index 485d81d9..d4226cee 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -9,7 +9,7 @@ back to the iOS 13 generation of targets. @Column { ```swift // SharingGRDB - @SharedReader(.fetch(Item.all)) + @SharedReader(.fetchAll(Item.all)) var items ``` } @@ -27,8 +27,7 @@ observed by SwiftUI so that views are recomputed when the external data changes, powered directly by SQLite using [Sharing](#What-is-Sharing) and [GRDB](#What-is-GRDB), and is usable from UIKit, `@Observable` models, and more. -> Note: It is not required to write queries as a raw SQL string, and a query builder can be used -> instead. For more information on SharingGRDB's querying capabilities, see . +> Note: For more information on SharingGRDB's querying capabilities, see . ## Quick start @@ -78,10 +77,10 @@ in SwiftData: > Note: For more information on preparing a SQLite database, see . This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies, like - [`fetchAll`](): +[`fetchAll`]( Date: Mon, 7 Apr 2025 11:51:08 -0700 Subject: [PATCH 077/171] wip --- Examples/Reminders/SearchReminders.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index d78c1731..d8bd2e2d 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -5,7 +5,7 @@ import SwiftUI struct SearchRemindersView: View { @SharedReader(value: 0) var completedCount: Int - @SharedReader(value: []) var reminders: [ReminderState] + @State.SharedReader(value: []) var reminders: [ReminderState] let searchText: String @State var showCompletedInSearchResults = false From ec57801b0a463bb28ef5e71f6ec44ab7c740b039 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 7 Apr 2025 14:00:03 -0700 Subject: [PATCH 078/171] wip --- Sources/SharingGRDB/StructuredQueries/StatementKey.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index d3447151..916d1db8 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -25,7 +25,7 @@ extension SharedReaderKey { return fetchAll(statement, database: database) } - public static func fetchAll( + public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil ) -> Self @@ -61,7 +61,7 @@ extension SharedReaderKey { @_disfavoredOverload public static func fetchAll< - S: SelectStatement, + S: StructuredQueriesCore.Statement, V1: QueryRepresentable, each V2: QueryRepresentable >( @@ -102,7 +102,7 @@ extension SharedReaderKey { return fetchAll(statement, database: database, scheduler: scheduler) } - public static func fetchAll( + public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable @@ -141,7 +141,7 @@ extension SharedReaderKey { @_disfavoredOverload public static func fetchAll< - S: SelectStatement, + S: StructuredQueriesCore.Statement, V1: QueryRepresentable, each V2: QueryRepresentable >( From 76e4989f48f6bbd79589cda261e2d0d0756ad60e Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 7 Apr 2025 17:39:29 -0700 Subject: [PATCH 079/171] wipo --- Examples/Reminders/RemindersListDetail.swift | 96 +++++++++++++++---- Examples/Reminders/RemindersLists.swift | 32 +++++-- Examples/Reminders/Schema.swift | 20 ++-- Examples/Reminders/SearchReminders.swift | 3 +- Package.swift | 3 +- .../xcshareddata/swiftpm/Package.resolved | 11 +-- 6 files changed, 120 insertions(+), 45 deletions(-) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 3b49c991..93788a75 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -7,16 +7,60 @@ struct RemindersListDetailView: View { @SharedReader(value: []) private var reminderStates: [ReminderState] @AppStorage private var ordering: Ordering @AppStorage private var showCompleted: Bool - private let remindersList: RemindersList @State var isNewReminderSheetPresented = false @Dependency(\.defaultDatabase) private var database - init(remindersList: RemindersList) { - self.remindersList = remindersList - _ordering = AppStorage(wrappedValue: .dueDate, "ordering_list_\(remindersList.id)") - _showCompleted = AppStorage(wrappedValue: false, "show_completed_list_\(remindersList.id)") + let detailType: DetailType + enum DetailType: Hashable { + case all + case completed + case flagged + case list(RemindersList) + case scheduled + case today + var tag: String { + switch self { + case .all: + "all" + case .completed: + "completed" + case .flagged: + "flagged" + case .list(let list): + "list_\(list.id)" + case .scheduled: + "scheduled" + case .today: + "today" + } + } + var navigationTitle: String { + switch self { + case .all: + "All" + case .completed: + "Completed" + case .flagged: + "Flagged" + case .list(let list): + list.name + case .scheduled: + "Scheduled" + case .today: + "Today" + } + } + } + + init(detailType: DetailType) { + self.detailType = detailType + _ordering = AppStorage(wrappedValue: .dueDate, "ordering_list_\(detailType.tag)") + _showCompleted = AppStorage( + wrappedValue: detailType != .completed, + "show_completed_list_\(detailType.tag)" + ) _reminderStates = SharedReader(wrappedValue: [], remindersKey) } @@ -26,7 +70,7 @@ struct RemindersListDetailView: View { ReminderRow( isPastDue: reminderState.isPastDue, reminder: reminderState.reminder, - remindersList: remindersList, + remindersList: reminderState.remindersList, tags: reminderState.tags ) } @@ -36,13 +80,14 @@ struct RemindersListDetailView: View { try await updateQuery() } } - .navigationTitle(remindersList.name) + .navigationTitle(detailType.navigationTitle) .navigationBarTitleDisplayMode(.large) - .sheet(isPresented: $isNewReminderSheetPresented) { - NavigationStack { - ReminderFormView(remindersList: remindersList) - } - } + // TODO: hide "Add reminder" button for certain detail types +// .sheet(isPresented: $isNewReminderSheetPresented) { +// NavigationStack { +// ReminderFormView(remindersList: remindersList) +// } +// } .toolbar { ToolbarItem(placement: .bottomBar) { HStack { @@ -108,7 +153,21 @@ struct RemindersListDetailView: View { fileprivate var remindersKey: some SharedReaderKey<[ReminderState]> { .fetchAll( Reminder - .where { $0.remindersListID == remindersList.id && (showCompleted || !$0.isCompleted) } + .where { + if !showCompleted { + !$0.isCompleted + } + } + .where { + switch detailType { + case .all: !$0.isCompleted + case .completed: $0.isCompleted + case .flagged: $0.isFlagged + case .list(let list): $0.remindersListID.eq(list.id) + case .scheduled: $0.isScheduled + case .today: $0.isToday + } + } .order { switch ordering { case .dueDate: @@ -120,9 +179,11 @@ struct RemindersListDetailView: View { } } .withTags + .join(RemindersList.all) { $0.remindersListID.eq($3.id) } .select { ReminderState.Columns( reminder: $0, + remindersList: $3, isPastDue: $0.isPastDue, commaSeparatedTags: $2.name.groupConcat() ) @@ -134,9 +195,10 @@ struct RemindersListDetailView: View { @Selection fileprivate struct ReminderState: Identifiable { var id: Reminder.ID { reminder.id } - var reminder: Reminder - var isPastDue: Bool - var commaSeparatedTags: String? + let reminder: Reminder + let remindersList: RemindersList + let isPastDue: Bool + let commaSeparatedTags: String? var tags: [String] { (commaSeparatedTags ?? "").split(separator: ",").map(String.init) } @@ -158,7 +220,7 @@ struct RemindersListDetailPreview: PreviewProvider { } } NavigationStack { - RemindersListDetailView(remindersList: remindersList) + RemindersListDetailView(detailType: .list(remindersList)) } } } diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 812fe8b6..4ee40ae4 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -42,11 +42,11 @@ struct RemindersListsView: View { .fetchOne( Reminder.select { Stats.Columns( - allCount: $0.count(), + allCount: $0.count(filter: !$0.isCompleted), completedCount: $0.count(filter: $0.isCompleted), flaggedCount: $0.count(filter: $0.isFlagged), - scheduledCount: $0.count(filter: #sql("date(\($0.date)) > date('now')")), - todayCount: $0.count(filter: #sql("date(\($0.date)) = date('now')")) + scheduledCount: $0.count(filter: $0.isScheduled), + todayCount: $0.count(filter: $0.isToday) ) } ) @@ -55,6 +55,7 @@ struct RemindersListsView: View { @State private var isAddListPresented = false @State private var searchText = "" + @State var remindersDetailType: RemindersListDetailView.DetailType? @Dependency(\.defaultDatabase) private var database @@ -69,13 +70,17 @@ struct RemindersListsView: View { count: stats.todayCount, iconName: "calendar.circle.fill", title: "Today" - ) {} + ) { + remindersDetailType = .today + } ReminderGridCell( color: .red, count: stats.scheduledCount, iconName: "calendar.circle.fill", title: "Scheduled" - ) {} + ) { + remindersDetailType = .scheduled + } } GridRow { ReminderGridCell( @@ -83,13 +88,17 @@ struct RemindersListsView: View { count: stats.allCount, iconName: "tray.circle.fill", title: "All" - ) {} + ) { + remindersDetailType = .all + } ReminderGridCell( color: .orange, count: stats.flaggedCount, iconName: "flag.circle.fill", title: "Flagged" - ) {} + ) { + remindersDetailType = .flagged + } } GridRow { ReminderGridCell( @@ -97,7 +106,9 @@ struct RemindersListsView: View { count: stats.completedCount, iconName: "checkmark.circle.fill", title: "Completed" - ) {} + ) { + remindersDetailType = .completed + } } } } @@ -106,7 +117,7 @@ struct RemindersListsView: View { Section { ForEach(remindersLists) { state in NavigationLink { - RemindersListDetailView(remindersList: state.remindersList) + RemindersListDetailView(detailType: .list(state.remindersList)) } label: { RemindersListRow( reminderCount: state.reminderCount, @@ -140,6 +151,9 @@ struct RemindersListsView: View { .presentationDetents([.medium]) } .searchable(text: $searchText) + .navigationDestination(item: $remindersDetailType) { detailType in + RemindersListDetailView(detailType: detailType) + } } } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 547a80d2..fd9d62f9 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -6,21 +6,21 @@ import StructuredQueriesGRDB @Table struct RemindersList: Hashable, Identifiable { - var id: Int64 + var id: Int var color = 0x4a99ef var name = "" } @Table struct Reminder: Equatable, Identifiable { - var id: Int64 + var id: Int @Column(as: Date.ISO8601Representation?.self) var date: Date? var isCompleted = false var isFlagged = false var notes = "" var priority: Priority? - var remindersListID: Int64 + var remindersListID: Int var title = "" static func searching(_ text: String) -> Where { Self.where { @@ -32,7 +32,13 @@ struct Reminder: Equatable, Identifiable { } extension Reminder.TableColumns { var isPastDue: some QueryExpression { - !isCompleted && #sql("coalesce(\(date), date('now')) < date('now')") + !isCompleted && #sql("date(\(date)) < date('now')") + } + var isToday: some QueryExpression { + !isCompleted && #sql("date(\(date)) = date('now')") + } + var isScheduled: some QueryExpression { + !isCompleted && #sql("date(\(date)) > date('now')") } } @@ -44,14 +50,14 @@ enum Priority: Int, QueryBindable { @Table struct Tag { - var id: Int64 + var id: Int var name = "" } @Table("remindersTags") struct ReminderTag { - var reminderID: Int64 - var tagID: Int64 + var reminderID: Int + var tagID: Int } func appDatabase() throws -> any DatabaseWriter { diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index d8bd2e2d..cbde8589 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -77,7 +77,8 @@ struct SearchRemindersView: View { } private var searchKey: some SharedReaderKey<[ReminderState]> { - let query = Reminder.searching(searchText) + let query = Reminder + .searching(searchText) .where { showCompletedInSearchResults || !$0.isCompleted } .order { ($0.isCompleted, $0.date) } .withTags diff --git a/Package.swift b/Package.swift index 8529eadb..521ad97a 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,8 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), +// .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), + .package(path: "../swift-structured-queries") ], targets: [ .target( diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5ecbd6ce..194880f8 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "141d3aed2910d045d6dc4900a02d3b45ca6b03d07df4afef17680976fc0c5a57", + "originHash" : "b4e43cc01acc325f3cdef305f5518542715038593aa8ae038b124411ea83e2bc", "pins" : [ { "identity" : "combine-schedulers", @@ -136,15 +136,6 @@ "version" : "1.18.3" } }, - { - "identity" : "swift-structured-queries", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-structured-queries", - "state" : { - "branch" : "main", - "revision" : "5e33730401f1ba2d2ed98e8d349d17d9480c4942" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", From 0607a7c6f5f41ced542f998dd188b1b472513709 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 7 Apr 2025 18:56:04 -0700 Subject: [PATCH 080/171] wip --- Examples/Reminders/ReminderRow.swift | 2 +- README.md | 32 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index ae687589..2513f649 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -87,7 +87,7 @@ struct ReminderRow: View { withErrorReporting { try database.write { db in try Reminder - .where { $0.id == reminder.id } + .where { $0.id.eq(reminder.id) } .update { $0.isCompleted.toggle() } .execute(db) } diff --git a/README.md b/README.md index f9dd76de..67209515 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ A lightweight replacement for SwiftData and `@Query`. [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fsharing-grdb%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pointfreeco/sharing-grdb) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fsharing-grdb%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/pointfreeco/sharing-grdb) -* [Learn more](#Learn-more) -* [Overview](#Overview) -* [Demos](#Demos) -* [Documentation](#Documentation) -* [Installation](#Installation) -* [Community](#Community) -* [License](#License) + * [Learn more](#Learn-more) + * [Overview](#Overview) + * [Demos](#Demos) + * [Documentation](#Documentation) + * [Installation](#Installation) + * [Community](#Community) + * [License](#License) ## Learn more @@ -119,10 +119,10 @@ struct MyApp: App {
> Note: For more information on preparing a SQLite database, see -[Preparing a SQLite database][preparing-db-article]. +> [Preparing a SQLite database][preparing-db-article]. This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies, like - [`fetchAll`][fetchall-docs]: +[`fetchAll`][fetchall-docs]: ```swift @SharedReader(.fetchAll(Item.all)) @@ -173,11 +173,11 @@ try modelContext.save() This is all you need to know to get started with SharingGRDB, but there's much more to learn. Read the [articles][articles] below to learn how to best utilize this library: -* [Fetching model data][fetching-article] -* [Observing changes to model data][observing-article] -* [Preparing a SQLite database][preparing-db-article] -* [Dynamic queries][dynamic-queries-article] -* [Comparison with SwiftData][comparison-swiftdata-article] + * [Fetching model data][fetching-article] + * [Observing changes to model data][observing-article] + * [Preparing a SQLite database][preparing-db-article] + * [Dynamic queries][dynamic-queries-article] + * [Comparison with SwiftData][comparison-swiftdata-article] [observing-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/observing [dynamic-queries-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/dynamicqueries @@ -230,8 +230,8 @@ Sharing. Check out [this](./Examples) directory to see them all, including: The documentation for releases and `main` are available here: -* [`main`](https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/) -* [0.1.0](https://swiftpackageindex.com/pointfreeco/sharing-grdb/0.1.0/documentation/sharinggrdb/) + * [`main`](https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/) + * [0.1.0](https://swiftpackageindex.com/pointfreeco/sharing-grdb/0.1.0/documentation/sharinggrdb/) ## Installation From 22d8648af41bc74199f92b9292c29ecc0eb8f88f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 7 Apr 2025 19:01:06 -0700 Subject: [PATCH 081/171] wip --- Examples/Reminders/Schema.swift | 28 +++++++++++++++--------- Examples/Reminders/SearchReminders.swift | 17 +++++++------- Examples/SyncUps/Schema.swift | 18 ++++++++++----- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index fd9d62f9..426792f1 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -84,17 +84,20 @@ func appDatabase() throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Add reminders lists table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "remindersLists" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "color" INTEGER NOT NULL DEFAULT \(raw: 0x4a99ef), "name" TEXT NOT NULL ) - """) - .execute(db) + """ + ) + .execute(db) } migrator.registerMigration("Add reminders table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "reminders" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "date" TEXT, @@ -107,18 +110,22 @@ func appDatabase() throws -> any DatabaseWriter { FOREIGN KEY("remindersListID") REFERENCES "remindersLists"("id") ON DELETE CASCADE ) - """) + """ + ) .execute(db) } migrator.registerMigration("Add tags table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "tags" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL COLLATE NOCASE UNIQUE ) - """) - .execute(db) - try #sql(""" + """ + ) + .execute(db) + try #sql( + """ CREATE TABLE "remindersTags" ( "reminderID" INTEGER NOT NULL, "tagID" INTEGER NOT NULL, @@ -126,7 +133,8 @@ func appDatabase() throws -> any DatabaseWriter { FOREIGN KEY("reminderID") REFERENCES "reminders"("id") ON DELETE CASCADE, FOREIGN KEY("tagID") REFERENCES "tags"("id") ON DELETE CASCADE ) - """) + """ + ) .execute(db) } #if DEBUG diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index cbde8589..2ada6020 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -97,17 +97,16 @@ struct SearchRemindersView: View { private func deleteCompletedReminders(monthsAgo: Int? = nil) { withErrorReporting { try database.write { db in - let baseQuery = Reminder + try Reminder .searching(searchText) .where(\.isCompleted) - if let monthsAgo { - try baseQuery - .where { #sql("\($0.date) < date('now', '-\(raw: monthsAgo) months')") } - .delete() - .execute(db) - } else { - try baseQuery.delete().execute(db) - } + .where { + if let monthsAgo { + #sql("\($0.date) < date('now', '-\(raw: monthsAgo) months')") + } + } + .delete() + .execute(db) } } } diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 3e46a11f..4714b0b2 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -100,18 +100,21 @@ func appDatabase() throws -> any DatabaseWriter { migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("Create sync-ups table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "syncUps" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "seconds" INTEGER NOT NULL DEFAULT 300, "theme" TEXT NOT NULL DEFAULT \(raw: Theme.bubblegum.rawValue), "title" TEXT NOT NULL ) - """) + """ + ) .execute(db) } migrator.registerMigration("Create attendees table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "attendees" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL, @@ -119,11 +122,13 @@ func appDatabase() throws -> any DatabaseWriter { FOREIGN KEY("syncUpID") REFERENCES "syncUps"("id") ON DELETE CASCADE ) - """) + """ + ) .execute(db) } migrator.registerMigration("Create meetings table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "meetings" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "date" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP UNIQUE, @@ -132,7 +137,8 @@ func appDatabase() throws -> any DatabaseWriter { FOREIGN KEY("syncUpID") REFERENCES "syncUps"("id") ON DELETE CASCADE ) - """) + """ + ) .execute(db) } #if DEBUG From 190d71b7673ff2445361f66447861159539bbcd6 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 8 Apr 2025 15:11:25 -0700 Subject: [PATCH 082/171] wip --- Examples/CaseStudies/Animations.swift | 8 +- Examples/CaseStudies/App.swift | 6 +- Examples/CaseStudies/DynamicQuery.swift | 13 +- .../CaseStudies/ObservableModelDemo.swift | 10 +- .../CaseStudies/SwiftDataTemplateDemo.swift | 6 +- Examples/CaseStudies/SwiftUIDemo.swift | 15 +- Examples/CaseStudies/TransactionDemo.swift | 17 +- Examples/CaseStudies/UIKitDemo.swift | 6 +- .../CaseStudiesTests/CaseStudiesTests.swift | 1 + Examples/Reminders/ReminderForm.swift | 2 +- Examples/Reminders/RemindersApp.swift | 2 +- Examples/Reminders/RemindersListDetail.swift | 10 +- Examples/Reminders/Schema.swift | 4 +- Examples/Reminders/SearchReminders.swift | 3 +- Examples/Reminders/TagsForm.swift | 10 +- Examples/SyncUps/Schema.swift | 2 +- Examples/SyncUps/SyncUpDetail.swift | 7 +- Examples/SyncUps/SyncUpsList.swift | 3 +- Package.swift | 2 +- .../StructuredQueries/StatementKey.swift | 247 +++++++++++++++++- .../StructuredQueriesGRDB.md | 8 + .../StructuredQueriesGRDBCore.md | 66 +++++ .../QueryCursor.swift | 5 +- .../Statement+GRDB.swift | 116 ++++++++ Tests/SharingGRDBTests/IntegrationTests.swift | 18 +- .../MigrationTests.swift | 6 +- 26 files changed, 533 insertions(+), 60 deletions(-) create mode 100644 Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md create mode 100644 Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md diff --git a/Examples/CaseStudies/Animations.swift b/Examples/CaseStudies/Animations.swift index 9692c81f..c6d6ef39 100644 --- a/Examples/CaseStudies/Animations.swift +++ b/Examples/CaseStudies/Animations.swift @@ -8,7 +8,7 @@ struct AnimationsCaseStudy: SwiftUICaseStudy { This demonstrates how to animate fetching data from the database, or when data changes in \ the database. Simply provide the `animation` argument to `fetchAll` (or the other querying \ tools, such as `fetch` and `fetchOne`). - + This is analogous to how animations work in SwiftData in which one provides an `animation` \ argument to the `@Query` macro. """ @@ -61,12 +61,14 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "facts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "body" TEXT NOT NULL ) - """) + """ + ) .execute(db) } try! migrator.migrate(databaseQueue) diff --git a/Examples/CaseStudies/App.swift b/Examples/CaseStudies/App.swift index 31ffed46..6463b491 100644 --- a/Examples/CaseStudies/App.swift +++ b/Examples/CaseStudies/App.swift @@ -6,9 +6,11 @@ struct CaseStudiesApp: App { var body: some Scene { WindowGroup { Form { - Text(""" + Text( + """ Open the preview in each case study file to run a case study. - """) + """ + ) } } } diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index 6db68708..38408481 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -9,7 +9,7 @@ struct DynamicQueryDemo: SwiftUICaseStudy { a fact about a number is loaded from the network and saved to a database. You can search the \ facts for text, and the list will stay in sync so that if a new fact is added to the database \ that satisfies the search term, it will immediately appear. - + To accomplish this one can invoke the `load` method defined on the `@SharedReader` projected \ value in order to set a new query with dynamic parameters. """ @@ -43,7 +43,7 @@ struct DynamicQueryDemo: SwiftUICaseStudy { withErrorReporting { try database.write { db in try Fact - .where{ $0.id.in(indexSet.compactMap { facts.facts[$0].id }) } + .where { $0.id.in(indexSet.compactMap { facts.facts[$0].id }) } .delete() .execute(db) } @@ -89,7 +89,8 @@ struct DynamicQueryDemo: SwiftUICaseStudy { var totalCount = 0 } func fetch(_ db: Database) throws -> Value { - let search = Fact + let search = + Fact .where { $0.body.contains(query) } .order { $0.id.desc() } return try Value( @@ -112,12 +113,14 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "facts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "body" TEXT NOT NULL ) - """) + """ + ) .execute(db) } try! migrator.migrate(databaseQueue) diff --git a/Examples/CaseStudies/ObservableModelDemo.swift b/Examples/CaseStudies/ObservableModelDemo.swift index 99edaad9..2a4f2426 100644 --- a/Examples/CaseStudies/ObservableModelDemo.swift +++ b/Examples/CaseStudies/ObservableModelDemo.swift @@ -8,7 +8,7 @@ struct ObservableModelDemo: SwiftUICaseStudy { This demonstrates how to use the `fetchAll` and `fetchOne` tools in an @Observable model. \ In SwiftUI, the `@Query` macro only works when installed directly in a SwiftUI view, and \ cannot be used outside of views. - + The tools provided with this library work basically anywhere, including in `@Observable` \ models and UIKit view controllers. """ @@ -82,7 +82,7 @@ private class Model { try database.write { db in try database.write { db in try Fact - .where{ $0.id.in(indices.compactMap { facts[$0].id }) } + .where { $0.id.in(indices.compactMap { facts[$0].id }) } .delete() .execute(db) } @@ -102,12 +102,14 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "facts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "body" TEXT NOT NULL ) - """) + """ + ) .execute(db) } try! migrator.migrate(databaseQueue) diff --git a/Examples/CaseStudies/SwiftDataTemplateDemo.swift b/Examples/CaseStudies/SwiftDataTemplateDemo.swift index d6824541..3c394bc9 100644 --- a/Examples/CaseStudies/SwiftDataTemplateDemo.swift +++ b/Examples/CaseStudies/SwiftDataTemplateDemo.swift @@ -68,12 +68,14 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create items table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "items" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "timestamp" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) - """) + """ + ) .execute(db) } try! migrator.migrate(databaseQueue) diff --git a/Examples/CaseStudies/SwiftUIDemo.swift b/Examples/CaseStudies/SwiftUIDemo.swift index eef801e7..2f30a649 100644 --- a/Examples/CaseStudies/SwiftUIDemo.swift +++ b/Examples/CaseStudies/SwiftUIDemo.swift @@ -8,7 +8,7 @@ struct SwiftUIDemo: SwiftUICaseStudy { This demonstrates how to use the `fetchAll` and `fetchOne` queries directly in a SwiftUI view. \ The tools listen for changes in the database so that when the table changes it automatically \ updates state and re-renders the view. - + You can also delete rows by swiping on a row and tapping the "Delete" button. """ let caseStudyTitle = "SwiftUI Views" @@ -46,7 +46,12 @@ struct SwiftUIDemo: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - try Fact.insert { $0.body } values: { fact }.execute(db) + try Fact.insert { + $0.body + } values: { + fact + } + .execute(db) } } } catch {} @@ -65,12 +70,14 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "facts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "body" TEXT NOT NULL ) - """) + """ + ) .execute(db) } try! migrator.migrate(databaseQueue) diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index 069ff5dd..cd305667 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -1,6 +1,6 @@ import Dependencies -import StructuredQueriesGRDB import SharingGRDB +import StructuredQueriesGRDB import SwiftUI struct TransactionDemo: SwiftUICaseStudy { @@ -9,7 +9,7 @@ struct TransactionDemo: SwiftUICaseStudy { database transaction. If you need to fetch multiple pieces of data from the database that \ all tend to change together, then performing those queries in a single transaction can be \ more performant. - + For example, if you need to fetch rows from a table as well as a count of the rows in the \ table, then those two pieces of data will tend to change at the same time (though not always). \ So, it can be better to perform the select and count as two different queries in the same \ @@ -47,7 +47,12 @@ struct TransactionDemo: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - try Fact.insert { $0.body } values: { fact }.execute(db) + try Fact.insert { + $0.body + } values: { + fact + } + .execute(db) } } } catch {} @@ -80,12 +85,14 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "facts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "body" TEXT NOT NULL ) - """) + """ + ) .execute(db) } try! migrator.migrate(databaseQueue) diff --git a/Examples/CaseStudies/UIKitDemo.swift b/Examples/CaseStudies/UIKitDemo.swift index 0378e101..b9edc1ad 100644 --- a/Examples/CaseStudies/UIKitDemo.swift +++ b/Examples/CaseStudies/UIKitDemo.swift @@ -126,12 +126,14 @@ extension DatabaseWriter where Self == DatabaseQueue { let databaseQueue = try! DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create 'facts' table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "facts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "body" TEXT NOT NULL ) - """) + """ + ) .execute(db) } try! migrator.migrate(databaseQueue) diff --git a/Examples/CaseStudiesTests/CaseStudiesTests.swift b/Examples/CaseStudiesTests/CaseStudiesTests.swift index 8c84261d..1d111ed1 100644 --- a/Examples/CaseStudiesTests/CaseStudiesTests.swift +++ b/Examples/CaseStudiesTests/CaseStudiesTests.swift @@ -1,4 +1,5 @@ import Testing + @testable import CaseStudies struct CaseStudiesTests { diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 7ea9e5ac..5ba8459a 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -171,7 +171,7 @@ struct ReminderFormView: View { guard let reminderID = try Reminder.upsert(reminder).returning(\.id).fetchOne(db) else { reportIssue("Could not upsert reminder") - return + return } if reminder.id != nil { try ReminderTag.where { $0.reminderID == reminderID } diff --git a/Examples/Reminders/RemindersApp.swift b/Examples/Reminders/RemindersApp.swift index 1ddd1e5d..c08b5c39 100644 --- a/Examples/Reminders/RemindersApp.swift +++ b/Examples/Reminders/RemindersApp.swift @@ -9,7 +9,7 @@ struct RemindersApp: App { $0.defaultDatabase = try Reminders.appDatabase() } } - + var body: some Scene { WindowGroup { NavigationStack { diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 93788a75..2381aff5 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -83,11 +83,11 @@ struct RemindersListDetailView: View { .navigationTitle(detailType.navigationTitle) .navigationBarTitleDisplayMode(.large) // TODO: hide "Add reminder" button for certain detail types -// .sheet(isPresented: $isNewReminderSheetPresented) { -// NavigationStack { -// ReminderFormView(remindersList: remindersList) -// } -// } + // .sheet(isPresented: $isNewReminderSheetPresented) { + // NavigationStack { + // ReminderFormView(remindersList: remindersList) + // } + // } .toolbar { ToolbarItem(placement: .bottomBar) { HStack { diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 426792f1..97cdd32c 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -107,7 +107,7 @@ func appDatabase() throws -> any DatabaseWriter { "priority" INTEGER, "remindersListID" INTEGER NOT NULL, "title" TEXT NOT NULL, - + FOREIGN KEY("remindersListID") REFERENCES "remindersLists"("id") ON DELETE CASCADE ) """ @@ -129,7 +129,7 @@ func appDatabase() throws -> any DatabaseWriter { CREATE TABLE "remindersTags" ( "reminderID" INTEGER NOT NULL, "tagID" INTEGER NOT NULL, - + FOREIGN KEY("reminderID") REFERENCES "reminders"("id") ON DELETE CASCADE, FOREIGN KEY("tagID") REFERENCES "tags"("id") ON DELETE CASCADE ) diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 2ada6020..8f28fa0b 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -77,7 +77,8 @@ struct SearchRemindersView: View { } private var searchKey: some SharedReaderKey<[ReminderState]> { - let query = Reminder + let query = + Reminder .searching(searchText) .where { showCompletedInSearchResults || !$0.isCompleted } .order { ($0.isCompleted, $0.date) } diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index a35b6fb9..ffc87f1d 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -45,17 +45,19 @@ struct TagsView: View { struct Tags: FetchKeyRequest { func fetch(_ db: Database) throws -> Value { - let top = try Tag + let top = + try Tag .group(by: \.id) - .join(ReminderTag.all) { $0.id.eq($1.tagID)} - .join(Reminder.all) { $1.reminderID.eq($2.id)} + .join(ReminderTag.all) { $0.id.eq($1.tagID) } + .join(Reminder.all) { $1.reminderID.eq($2.id) } .having { $2.count().gt(0) } .order { ($2.count().desc(), $0.name) } .limit(3) .select { tags, _, _ in tags } .fetchAll(db) - let rest = try Tag + let rest = + try Tag .where { !$0.id.in(top.map(\.id)) } .fetchAll(db) diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 4714b0b2..fd45fefa 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -134,7 +134,7 @@ func appDatabase() throws -> any DatabaseWriter { "date" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP UNIQUE, "syncUpID" INTEGER NOT NULL, "transcript" TEXT NOT NULL, - + FOREIGN KEY("syncUpID") REFERENCES "syncUps"("id") ON DELETE CASCADE ) """ diff --git a/Examples/SyncUps/SyncUpDetail.swift b/Examples/SyncUps/SyncUpDetail.swift index 5bf30ea5..329f285a 100644 --- a/Examples/SyncUps/SyncUpDetail.swift +++ b/Examples/SyncUps/SyncUpDetail.swift @@ -112,7 +112,8 @@ final class SyncUpDetailModel: HashableObject { else { throw NotFound() } return try Value( attendees: Attendee.where { $0.syncUpID == syncUp.id }.fetchAll(db), - meetings: Meeting + meetings: + Meeting .where { $0.syncUpID == syncUp.id } .order { $0.date.desc() } .fetchAll(db), @@ -158,7 +159,9 @@ struct SyncUpDetailView: View { if !model.details.meetings.isEmpty { Section { ForEach(model.details.meetings, id: \.id) { meeting in - NavigationLink(value: AppModel.Path.meeting(meeting, attendees: model.details.attendees)) { + NavigationLink( + value: AppModel.Path.meeting(meeting, attendees: model.details.attendees) + ) { HStack { Image(systemName: "calendar") Text(meeting.date, style: .date) diff --git a/Examples/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUpsList.swift index 952af551..119ae68d 100644 --- a/Examples/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUpsList.swift @@ -7,7 +7,8 @@ import SwiftUINavigation @Observable final class SyncUpsListModel { var addSyncUp: SyncUpFormModel? - @ObservationIgnored @SharedReader( + @ObservationIgnored + @SharedReader( .fetchAll( SyncUp .group(by: \.id) diff --git a/Package.swift b/Package.swift index 521ad97a..b27a82d7 100644 --- a/Package.swift +++ b/Package.swift @@ -72,7 +72,7 @@ let package = Package( .product(name: "DependenciesTestSupport", package: "swift-dependencies"), .product(name: "StructuredQueries", package: "swift-structured-queries"), ] - ) + ), ], swiftLanguageModes: [.v6] ) diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 916d1db8..3f0ce730 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -12,6 +12,19 @@ import StructuredQueriesGRDBCore // MARK: Basics extension SharedReaderKey { + /// A key that can query for a collection of data in a SQLite database. + /// + /// This key takes a query built using the Structured Queries library. + /// + /// ```swift + /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items + /// ``` + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil @@ -25,6 +38,19 @@ extension SharedReaderKey { return fetchAll(statement, database: database) } + /// A key that can query for a collection of data in a SQLite database. + /// + /// This key takes a query built using the Structured Queries library. + /// + /// ```swift + /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items + /// ``` + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil @@ -33,6 +59,19 @@ extension SharedReaderKey { fetch(FetchAllStatementValueRequest(statement: statement), database: database) } + /// A key that can query for a value in a SQLite database. + /// + /// This key takes a query built using the Structured Queries library. + /// + /// ```swift + /// @SharedReader(.fetchOne(Item.count())) var itemCount = 0 + /// ``` + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchOne( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil @@ -46,7 +85,21 @@ extension SharedReaderKey { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { + /// A key that can query for a collection of data in a SQLite database. + /// + /// This key takes a query built using the Structured Queries library. + /// + /// ```swift + /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items + /// ``` + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload + @_documentation(visibility: private) public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil @@ -59,7 +112,21 @@ extension SharedReaderKey { fetchAll(statement.selectStar(), database: database) } + /// A key that can query for a collection of data in a SQLite database. + /// + /// This key takes a query built using the Structured Queries library. + /// + /// ```swift + /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items + /// ``` + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload + @_documentation(visibility: private) public static func fetchAll< S: StructuredQueriesCore.Statement, V1: QueryRepresentable, @@ -75,7 +142,21 @@ extension SharedReaderKey { fetch(FetchAllStatementPackRequest(statement: statement), database: database) } + /// A key that can query for a value in a SQLite database. + /// + /// This key takes a query built using the Structured Queries library. + /// + /// ```swift + /// @SharedReader(.fetchOne(Item.count())) var itemCount = 0 + /// ``` + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload + @_documentation(visibility: private) public static func fetchOne( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil @@ -88,6 +169,18 @@ extension SharedReaderKey { // MARK: - Scheduling extension SharedReaderKey { + /// A key that can query for a collection of data in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a + /// scheduler. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil, @@ -102,22 +195,50 @@ extension SharedReaderKey { return fetchAll(statement, database: database, scheduler: scheduler) } + /// A key that can query for a collection of data in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a + /// scheduler. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable ) -> Self where S.QueryValue: QueryRepresentable, Self == FetchKey<[S.QueryValue.QueryOutput]>.Default { - fetch(FetchAllStatementValueRequest(statement: statement), database: database, scheduler: scheduler) + fetch( + FetchAllStatementValueRequest(statement: statement), database: database, scheduler: scheduler + ) } + /// A key that can query for a value in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a + /// scheduler. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchOne( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable ) -> Self where Self == FetchKey { - fetch(FetchOneStatementValueRequest(statement: statement), database: database, scheduler: scheduler) + fetch( + FetchOneStatementValueRequest(statement: statement), database: database, scheduler: scheduler + ) } } @@ -125,7 +246,20 @@ extension SharedReaderKey { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { + /// A key that can query for a collection of data in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a + /// scheduler. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload + @_documentation(visibility: private) public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil, @@ -139,7 +273,20 @@ extension SharedReaderKey { fetchAll(statement.selectStar(), database: database, scheduler: scheduler) } + /// A key that can query for a collection of data in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a + /// scheduler. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload + @_documentation(visibility: private) public static func fetchAll< S: StructuredQueriesCore.Statement, V1: QueryRepresentable, @@ -153,17 +300,34 @@ extension SharedReaderKey { S.QueryValue == (V1, repeat each V2), Self == FetchKey<[(V1.QueryOutput, repeat (each V2).QueryOutput)]>.Default { - fetch(FetchAllStatementPackRequest(statement: statement), database: database, scheduler: scheduler) + fetch( + FetchAllStatementPackRequest(statement: statement), database: database, scheduler: scheduler + ) } + /// A key that can query for a value in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a + /// scheduler. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload + @_documentation(visibility: private) public static func fetchOne( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable ) -> Self where Self == FetchKey<(repeat (each Value).QueryOutput)> { - fetch(FetchOneStatementPackRequest(statement: statement), database: database, scheduler: scheduler) + fetch( + FetchOneStatementPackRequest(statement: statement), database: database, scheduler: scheduler + ) } } @@ -171,6 +335,18 @@ extension SharedReaderKey { #if canImport(SwiftUI) extension SharedReaderKey { + /// A key that can query for a collection of data in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a + /// SwiftUI animation. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil, @@ -185,6 +361,18 @@ extension SharedReaderKey { return fetchAll(statement, database: database, animation: animation) } + /// A key that can query for a collection of data in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a + /// SwiftUI animation. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchAll( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, @@ -198,6 +386,18 @@ extension SharedReaderKey { ) } + /// A key that can query for a collection of value in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a + /// SwiftUI animation. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchOne( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, @@ -216,7 +416,20 @@ extension SharedReaderKey { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SharedReaderKey { + /// A key that can query for a collection of data in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a + /// SwiftUI animation. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload + @_documentation(visibility: private) public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil, @@ -230,7 +443,20 @@ extension SharedReaderKey { fetchAll(statement.selectStar(), database: database, animation: animation) } + /// A key that can query for a collection of data in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a + /// SwiftUI animation. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload + @_documentation(visibility: private) public static func fetchAll( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, @@ -244,7 +470,20 @@ extension SharedReaderKey { ) } + /// A key that can query for a value in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a + /// SwiftUI animation. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the + /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload + @_documentation(visibility: private) public static func fetchOne( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil, diff --git a/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md b/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md new file mode 100644 index 00000000..4640cdc6 --- /dev/null +++ b/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md @@ -0,0 +1,8 @@ +# ``StructuredQueriesGRDB`` + +A library interfacing Structured Queries with GRDB. + +## Overview + +The core functionality of this library is defined in + [`StructuredQueriesGRDBCore`](structuredqueriesgrdbcore). diff --git a/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md new file mode 100644 index 00000000..b3e09eab --- /dev/null +++ b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md @@ -0,0 +1,66 @@ +# ``StructuredQueriesGRDBCore`` + +The core functionality of interfacing Structured Queries with GRDB. This module is automatically +imported when you `import StructuredQueriesGRDB`. + +## Overview + +This library can be used to directly execute queries built using the [Structured Queries][sq-gh] +library and a [GRDB][grdb-gh] database. + +While the `SharingGRDB` module provides tools to observe queries using the `@SharedReader` property +wrapper, you will also want to execute one-off queries directly, without Sharing's APIs, especially +when it comes to `INSERT`, `UPDATE`, and `DELETE` statements. This module extends Structured +Queries' `Statement` type with `execute`, `fetchAll`, `fetchOne`, and `fetchCount` methods that +execute the query on a given GRDB database. + +```swift +@Table +struct Player { + let id: Int + var name = "" + var score = 0 +} + +try #sql( + """ + CREATE TABLE players ( + id INTEGER PRIMARY KEY, + name TEXT, + score INTEGER + ) + """ +) +.execute(db) + +let players = Player + .where { $0.score > 10 } + .fetchAll(db) +// SELECT … FROM "players" +// WHERE "players"."score" > 10 + +let averageScore = try Player + .select { $0.score.avg() } + .fetchOne(db) +// SELECT avg("players"."score") FROM "players" +``` + +For more information on how to build queries, see the [Structured Queries documentation][sq-spi]. + +[sq-gh]: https://github.com/pointfreeco/swift-structured-queries +[sq-spi]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueries +[grdb-gh]: https://github.com/groue/GRDB.swift + +## Topics + +### Executing statements + +- ``StructuredQueriesCore/Statement/execute(_:)`` +- ``StructuredQueriesCore/Statement/fetchAll(_:)-4glz5`` +- ``StructuredQueriesCore/Statement/fetchOne(_:)-3mdmq`` +- ``StructuredQueriesCore/Statement/fetchCursor(_:)-5bk5y`` +- ``StructuredQueriesCore/SelectStatement/fetchCount(_:)`` + +### Iterating over rows + +- ``QueryCursor`` diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index 5b96b06a..e3ef1f9c 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -3,6 +3,9 @@ import GRDB import SQLite3 import StructuredQueriesCore +/// A cursor of a structured query. +/// +/// Iterates over and decodes all of the rows of a structured query. public class QueryCursor: DatabaseCursor { public var _isDone = false public let _statement: GRDB.Statement @@ -96,7 +99,7 @@ extension QueryBinding { return .null case let .text(text): return text.databaseValue - case let ._invalid(error): + case let .invalid(error): throw error } } diff --git a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift index 25b58fcc..4775a093 100644 --- a/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift +++ b/Sources/StructuredQueriesGRDBCore/Statement+GRDB.swift @@ -3,11 +3,42 @@ import SQLite3 import StructuredQueriesCore extension StructuredQueriesCore.Statement { + /// Executes a structured query on the given database connection. + /// + /// For example: + /// + /// ```swift + /// try database.write { db in + /// try Player.insert { $0.name } values: { "Arthur" } + /// .execute(db) + /// // INSERT INTO "players" ("name") + /// // VALUES ('Arthur'); + /// } + /// ``` + /// + /// - Parameter db: A database connection. @inlinable public func execute(_ db: Database) throws where QueryValue == () { try QueryVoidCursor(db: db, query: query).next() } + /// Returns an array of all values fetched from the database. + /// + /// For example: + /// + /// ```swift + /// let players = try database.read { db in + /// let lastName = "O'Reilly" + /// try Player + /// .where { $0.lastName == lastName } + /// .fetchAll(db) + /// // SELECT … FROM "players" + /// // WHERE "players"."lastName" = 'O''Reilly' + /// } + /// ``` + /// + /// - Parameter db: A database connection. + /// - Returns: An array of all values decoded from the database. @inlinable public func fetchAll(_ db: Database) throws -> [QueryValue.QueryOutput] where QueryValue: QueryRepresentable { @@ -17,12 +48,48 @@ extension StructuredQueriesCore.Statement { return output } + /// Returns a single value fetched from the database. + /// + /// For example: + /// + /// ```swift + /// let player = try database.read { db in + /// let lastName = "O'Reilly" + /// try Player + /// .where { $0.lastName == lastName } + /// .limit(1) + /// .fetchOne(db) + /// // SELECT … FROM "players" + /// // WHERE "players"."lastName" = 'O''Reilly' + /// // LIMIT 1 + /// } + /// ``` + /// + /// - Parameter db: A database connection. + /// - Returns: A single value decoded from the database. @inlinable public func fetchOne(_ db: Database) throws -> QueryValue.QueryOutput? where QueryValue: QueryRepresentable { try fetchCursor(db).next() } + /// Returns a cursor to all values fetched from the database. + /// + /// For example: + /// + /// ```swift + /// try database.read { db in + /// let lastName = "O'Reilly" + /// let query = Player.where { $0.lastName == lastName } + /// let players = try query.fetchCursor(db) + /// while let player = try players.next() { + /// print(player.name) + /// } + /// } + /// ``` + /// + /// - Parameter db: A database connection. + /// - Returns: A cursor to all values decoded from the database. @inlinable public func fetchCursor(_ db: Database) throws -> QueryCursor where QueryValue: QueryRepresentable { @@ -32,6 +99,11 @@ extension StructuredQueriesCore.Statement { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension StructuredQueriesCore.Statement { + /// 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. + @_documentation(visibility: private) @inlinable public func fetchAll( _ db: Database @@ -41,6 +113,11 @@ extension StructuredQueriesCore.Statement { return try Array(cursor) } + /// Returns a single value fetched from the database. + /// + /// - Parameter db: A database connection. + /// - Returns: A single value decoded from the database. + @_documentation(visibility: private) @inlinable public func fetchOne( _ db: Database @@ -50,6 +127,11 @@ extension StructuredQueriesCore.Statement { return try cursor.next() } + /// 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. + @_documentation(visibility: private) @inlinable public func fetchCursor( _ db: Database @@ -60,6 +142,10 @@ extension StructuredQueriesCore.Statement { } extension SelectStatement where QueryValue == (), Joins == () { + /// 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 func fetchCount(_ db: Database) throws -> Int { let query = asSelect().count() @@ -68,6 +154,11 @@ extension SelectStatement where QueryValue == (), Joins == () { } extension SelectStatement where QueryValue == (), Joins == () { + /// 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. + @_documentation(visibility: private) @inlinable public func fetchAll(_ db: Database) throws -> [From.QueryOutput] { let cursor = try QueryValueCursor(db: db, query: query) @@ -76,11 +167,21 @@ extension SelectStatement where QueryValue == (), Joins == () { return output } + /// Returns a single value fetched from the database. + /// + /// - Parameter db: A database connection. + /// - Returns: A single value decoded from the database. + @_documentation(visibility: private) @inlinable public func fetchOne(_ db: Database) throws -> From.QueryOutput? { try fetchCursor(db).next() } + /// 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. + @_documentation(visibility: private) @inlinable public func fetchCursor(_ db: Database) throws -> QueryCursor { try QueryValueCursor(db: db, query: query) @@ -89,6 +190,11 @@ extension SelectStatement where QueryValue == (), Joins == () { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SelectStatement where QueryValue == () { + /// 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. + @_documentation(visibility: private) @inlinable public func fetchAll( _ db: Database @@ -97,6 +203,11 @@ extension SelectStatement where QueryValue == () { try Array(fetchCursor(db)) } + /// Returns a single value fetched from the database. + /// + /// - Parameter db: A database connection. + /// - Returns: A single value decoded from the database. + @_documentation(visibility: private) @inlinable public func fetchOne( _ db: Database @@ -105,6 +216,11 @@ extension SelectStatement where QueryValue == () { try fetchCursor(db).next() } + /// 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. + @_documentation(visibility: private) @inlinable public func fetchCursor( _ db: Database diff --git a/Tests/SharingGRDBTests/IntegrationTests.swift b/Tests/SharingGRDBTests/IntegrationTests.swift index 80c5655f..60aec332 100644 --- a/Tests/SharingGRDBTests/IntegrationTests.swift +++ b/Tests/SharingGRDBTests/IntegrationTests.swift @@ -76,28 +76,32 @@ private struct Attendee: Equatable { var syncUpID: SyncUp.ID } -private extension DatabaseWriter where Self == DatabaseQueue { - static func syncUps() throws -> Self { +extension DatabaseWriter where Self == DatabaseQueue { + fileprivate static func syncUps() throws -> Self { let database = try DatabaseQueue() var migrator = DatabaseMigrator() migrator.registerMigration("Create schema") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "syncUps" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "isActive" INTEGER NOT NULL, "title" TEXT NOT NULL ) - """) + """ + ) .execute(db) - try #sql(""" + try #sql( + """ CREATE TABLE "attendees" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "syncUpID" INTEGER NOT NULL, "name" TEXT NOT NULL, - + FOREIGN KEY("syncUpID") REFERENCES "syncUps"("id") ) - """) + """ + ) .execute(db) } try migrator.migrate(database) diff --git a/Tests/StructuredQueriesGRDBTests/MigrationTests.swift b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift index 2c487af3..653b00f6 100644 --- a/Tests/StructuredQueriesGRDBTests/MigrationTests.swift +++ b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift @@ -8,11 +8,13 @@ import Testing @Test func dates() throws { let database = try DatabaseQueue() try database.write { db in - try #sql(""" + try #sql( + """ CREATE TABLE "models" ( "date" TEXT NOT NULL ) - """) + """ + ) .execute(db) } From b7c81d1a1ee2d0d14f64012cdc3d7ddd349bde89 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 8 Apr 2025 15:12:15 -0700 Subject: [PATCH 083/171] wip --- Package.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index b27a82d7..b627aebf 100644 --- a/Package.swift +++ b/Package.swift @@ -29,8 +29,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.4"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), -// .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), - .package(path: "../swift-structured-queries") + .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), ], targets: [ .target( From 084e8227d0c0204fe8621f7297ee01f700606f06 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 10 Apr 2025 16:05:20 -0700 Subject: [PATCH 084/171] clean up --- Examples/Examples.xcodeproj/project.pbxproj | 8 ++ Examples/Reminders/RemindersListDetail.swift | 131 ++++++++++--------- 2 files changed, 77 insertions(+), 62 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index efb48c78..b13fd052 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + CA14DBC92DA884C400E36852 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = CA14DBC82DA884C400E36852 /* CasePaths */; }; CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA2908C82D4AF70E003F165F /* UIKitNavigation */; }; CAD001872D874F1F00FA977A /* DependenciesTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = CAD001862D874F1F00FA977A /* DependenciesTestSupport */; }; CAE6C64D2D69017D00CE1C90 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */; }; @@ -140,6 +141,7 @@ files = ( CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */, CAE6C64D2D69017D00CE1C90 /* StructuredQueriesGRDB in Frameworks */, + CA14DBC92DA884C400E36852 /* CasePaths in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -285,6 +287,7 @@ packageProductDependencies = ( CAFDD6492D5E823A00EE099E /* SharingGRDB */, CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */, + CA14DBC82DA884C400E36852 /* CasePaths */, ); productName = Reminders; productReference = CAF836D82D4735AB0047AEB5 /* Reminders.app */; @@ -910,6 +913,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + CA14DBC82DA884C400E36852 /* CasePaths */ = { + isa = XCSwiftPackageProductDependency; + package = DCBE8A122D4842BF0071F499 /* XCRemoteSwiftPackageReference "swift-case-paths" */; + productName = CasePaths; + }; CA2908C82D4AF70E003F165F /* UIKitNavigation */ = { isa = XCSwiftPackageProductDependency; package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 2381aff5..9e465061 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -1,3 +1,4 @@ +import CasePaths import Sharing import SharingGRDB import StructuredQueriesGRDB @@ -8,52 +9,11 @@ struct RemindersListDetailView: View { @AppStorage private var ordering: Ordering @AppStorage private var showCompleted: Bool + let detailType: DetailType @State var isNewReminderSheetPresented = false @Dependency(\.defaultDatabase) private var database - let detailType: DetailType - enum DetailType: Hashable { - case all - case completed - case flagged - case list(RemindersList) - case scheduled - case today - var tag: String { - switch self { - case .all: - "all" - case .completed: - "completed" - case .flagged: - "flagged" - case .list(let list): - "list_\(list.id)" - case .scheduled: - "scheduled" - case .today: - "today" - } - } - var navigationTitle: String { - switch self { - case .all: - "All" - case .completed: - "Completed" - case .flagged: - "Flagged" - case .list(let list): - list.name - case .scheduled: - "Scheduled" - case .today: - "Today" - } - } - } - init(detailType: DetailType) { self.detailType = detailType _ordering = AppStorage(wrappedValue: .dueDate, "ordering_list_\(detailType.tag)") @@ -82,26 +42,29 @@ struct RemindersListDetailView: View { } .navigationTitle(detailType.navigationTitle) .navigationBarTitleDisplayMode(.large) - // TODO: hide "Add reminder" button for certain detail types - // .sheet(isPresented: $isNewReminderSheetPresented) { - // NavigationStack { - // ReminderFormView(remindersList: remindersList) - // } - // } + .sheet(isPresented: $isNewReminderSheetPresented) { + if let remindersList = detailType.list { + NavigationStack { + ReminderFormView(remindersList: remindersList) + } + } + } .toolbar { - ToolbarItem(placement: .bottomBar) { - HStack { - Button { - isNewReminderSheetPresented = true - } label: { - HStack { - Image(systemName: "plus.circle.fill") - Text("New reminder") + if detailType.is(\.list) { + ToolbarItem(placement: .bottomBar) { + HStack { + Button { + isNewReminderSheetPresented = true + } label: { + HStack { + Image(systemName: "plus.circle.fill") + Text("New reminder") + } + .bold() + .font(.title3) } - .bold() - .font(.title3) + Spacer() } - Spacer() } } ToolbarItem(placement: .primaryAction) { @@ -146,6 +109,49 @@ struct RemindersListDetailView: View { } } + @CasePathable + @dynamicMemberLookup + enum DetailType: Hashable { + case all + case completed + case flagged + case list(RemindersList) + case scheduled + case today + var tag: String { + switch self { + case .all: + "all" + case .completed: + "completed" + case .flagged: + "flagged" + case .list(let list): + "list_\(list.id)" + case .scheduled: + "scheduled" + case .today: + "today" + } + } + var navigationTitle: String { + switch self { + case .all: + "All" + case .completed: + "Completed" + case .flagged: + "Flagged" + case .list(let list): + list.name + case .scheduled: + "Scheduled" + case .today: + "Today" + } + } + } + private func updateQuery() async throws { try await $reminderStates.load(remindersKey) } @@ -168,14 +174,15 @@ struct RemindersListDetailView: View { case .today: $0.isToday } } + .order { $0.isCompleted } .order { switch ordering { case .dueDate: - ($0.isCompleted, $0.date) + $0.date case .priority: - ($0.isCompleted, $0.priority.desc(), $0.isFlagged.desc()) + ($0.priority.desc(), $0.isFlagged.desc()) case .title: - ($0.isCompleted, $0.title) + $0.title } } .withTags From 0722e328299e74c2f6659fcb7722298f1d3df4b1 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 12 Apr 2025 08:29:25 -0500 Subject: [PATCH 085/171] wip --- Examples/Reminders/ReminderForm.swift | 1 + Examples/Reminders/RemindersLists.swift | 2 ++ Examples/Reminders/Schema.swift | 36 ++++++++++--------- .../xcshareddata/swiftpm/Package.resolved | 11 +++++- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 5ba8459a..a7dbbc09 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -76,6 +76,7 @@ struct ReminderFormView: View { Text("Date") } } + // TODO: Try `Binding.init?` initializer if let date = reminder.date { DatePicker( "", diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 4ee40ae4..4101f774 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -30,6 +30,8 @@ struct RemindersListsView: View { .select { ReminderListState.Columns( reminderCount: #sql("count(iif(\($1.isCompleted), NULL, \($1.id)))"), +// reminderCount: $1.count(filter: !($1.isCompleted ?? false)), +// reminderCount: $1.count(filter: !$1.isCompleted.ifnull(false)), remindersList: $0 ) }, diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 97cdd32c..2c3fbcac 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -156,69 +156,69 @@ func appDatabase() throws -> any DatabaseWriter { } func createDebugRemindersLists() throws { + try RemindersList.delete().execute(self) try RemindersList.insert { - ($0.color, $0.name) - } values: { - (color: 0x4a99ef, name: "Personal") - (color: 0xed8935, name: "Family") - (color: 0xb25dd3, name: "Business") + RemindersList.Draft(color: 0x4a99ef, name: "Personal") + RemindersList.Draft(color: 0xed8935, name: "Family") + RemindersList.Draft(color: 0xb25dd3, name: "Business") } .execute(self) } func createDebugReminders() throws { - try Reminder.insert([ + try Reminder.delete().execute(self) + try Reminder.insert { Reminder.Draft( date: Date(), notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", remindersListID: 1, title: "Groceries" - ), + ) Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 2), isFlagged: true, remindersListID: 1, title: "Haircut" - ), + ) Reminder.Draft( date: Date(), notes: "Ask about diet", priority: .high, remindersListID: 1, title: "Doctor appointment" - ), + ) Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 190), isCompleted: true, remindersListID: 1, title: "Take a walk" - ), + ) Reminder.Draft( date: Date(), remindersListID: 1, title: "Buy concert tickets" - ), + ) Reminder.Draft( date: Date().addingTimeInterval(60 * 60 * 24 * 2), isFlagged: true, priority: .high, remindersListID: 2, title: "Pick up kids from school" - ), + ) Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, priority: .low, remindersListID: 2, title: "Get laundry" - ), + ) Reminder.Draft( date: Date().addingTimeInterval(60 * 60 * 24 * 4), isCompleted: false, priority: .high, remindersListID: 2, title: "Take out trash" - ), + ) Reminder.Draft( date: Date().addingTimeInterval(60 * 60 * 24 * 2), notes: """ @@ -228,19 +228,21 @@ func appDatabase() throws -> any DatabaseWriter { """, remindersListID: 3, title: "Call accountant" - ), + ) Reminder.Draft( date: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, priority: .medium, remindersListID: 3, title: "Send weekly emails" - ), - ]) + ) + } .execute(self) } func createDebugTags() throws { + try ReminderTag.delete().execute(self) + try Tag.delete().execute(self) try Tag.insert(\.name) { "car" "kids" diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 194880f8..59a525c9 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b4e43cc01acc325f3cdef305f5518542715038593aa8ae038b124411ea83e2bc", + "originHash" : "2dfe1f5cc9897e682ed5b52e8734901c9a06e2ced64484fc2a0742abac1657e7", "pins" : [ { "identity" : "combine-schedulers", @@ -136,6 +136,15 @@ "version" : "1.18.3" } }, + { + "identity" : "swift-structured-queries", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-structured-queries", + "state" : { + "branch" : "main", + "revision" : "6da773bfe57eb0a81d1d8271ff09f1c7d0e21764" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", From 005dd2eadf0adf03c9218cee25d2a843a3181e7c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 12 Apr 2025 09:44:37 -0500 Subject: [PATCH 086/171] Clean uop --- Examples/Reminders/ReminderForm.swift | 8 ++-- Examples/Reminders/ReminderRow.swift | 4 +- Examples/Reminders/RemindersListDetail.swift | 9 ++--- Examples/Reminders/Schema.swift | 37 +++++++++---------- Examples/Reminders/SearchReminders.swift | 4 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- 6 files changed, 30 insertions(+), 34 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index a7dbbc09..0663ee65 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -77,10 +77,10 @@ struct ReminderFormView: View { } } // TODO: Try `Binding.init?` initializer - if let date = reminder.date { + if let dueDate = reminder.dueDate { DatePicker( "", - selection: $reminder.date[coalesce: date], + selection: $reminder.dueDate[coalesce: dueDate], displayedComponents: [.date, .hourAndMinute] ) .padding([.top, .bottom], 2) @@ -195,8 +195,8 @@ struct ReminderFormView: View { extension Reminder.Draft { fileprivate var isDateSet: Bool { - get { date != nil } - set { date = newValue ? Date() : nil } + get { dueDate != nil } + set { dueDate = newValue ? Date() : nil } } } diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 2513f649..07781d3b 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -95,7 +95,7 @@ struct ReminderRow: View { } private var dueText: Text { - if let date = reminder.date { + if let date = reminder.dueDate { Text(date.formatted(date: .numeric, time: .shortened)) .foregroundStyle(isPastDue ? .red : .gray) } else { @@ -104,7 +104,7 @@ struct ReminderRow: View { } private var subtitleText: Text { - let tagsText = tags.reduce(Text(reminder.date == nil ? "" : " ")) { result, tag in + let tagsText = tags.reduce(Text(reminder.dueDate == nil ? "" : " ")) { result, tag in result + Text("#\(tag) ") .foregroundStyle(.gray) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 9e465061..b041c2dd 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -177,12 +177,9 @@ struct RemindersListDetailView: View { .order { $0.isCompleted } .order { switch ordering { - case .dueDate: - $0.date - case .priority: - ($0.priority.desc(), $0.isFlagged.desc()) - case .title: - $0.title + case .dueDate: $0.dueDate + case .priority: ($0.priority.desc(), $0.isFlagged.desc()) + case .title: $0.title } } .withTags diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 2c3fbcac..5669bb26 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -15,7 +15,7 @@ struct RemindersList: Hashable, Identifiable { struct Reminder: Equatable, Identifiable { var id: Int @Column(as: Date.ISO8601Representation?.self) - var date: Date? + var dueDate: Date? var isCompleted = false var isFlagged = false var notes = "" @@ -32,13 +32,13 @@ struct Reminder: Equatable, Identifiable { } extension Reminder.TableColumns { var isPastDue: some QueryExpression { - !isCompleted && #sql("date(\(date)) < date('now')") + !isCompleted && #sql("coalesce(date(\(dueDate)) < date('now'), 0)") } var isToday: some QueryExpression { - !isCompleted && #sql("date(\(date)) = date('now')") + !isCompleted && #sql("coalesce(date(\(dueDate)) = date('now'), 0)") } var isScheduled: some QueryExpression { - !isCompleted && #sql("date(\(date)) > date('now')") + !isCompleted && dueDate.isNot(nil) } } @@ -90,7 +90,7 @@ func appDatabase() throws -> any DatabaseWriter { "id" INTEGER PRIMARY KEY AUTOINCREMENT, "color" INTEGER NOT NULL DEFAULT \(raw: 0x4a99ef), "name" TEXT NOT NULL - ) + ) STRICT """ ) .execute(db) @@ -100,7 +100,7 @@ func appDatabase() throws -> any DatabaseWriter { """ CREATE TABLE "reminders" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "date" TEXT, + "dueDate" TEXT, "isCompleted" INTEGER NOT NULL DEFAULT 0, "isFlagged" INTEGER NOT NULL DEFAULT 0, "notes" TEXT, @@ -109,7 +109,7 @@ func appDatabase() throws -> any DatabaseWriter { "title" TEXT NOT NULL, FOREIGN KEY("remindersListID") REFERENCES "remindersLists"("id") ON DELETE CASCADE - ) + ) STRICT """ ) .execute(db) @@ -120,7 +120,7 @@ func appDatabase() throws -> any DatabaseWriter { CREATE TABLE "tags" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL COLLATE NOCASE UNIQUE - ) + ) STRICT """ ) .execute(db) @@ -132,7 +132,7 @@ func appDatabase() throws -> any DatabaseWriter { FOREIGN KEY("reminderID") REFERENCES "reminders"("id") ON DELETE CASCADE, FOREIGN KEY("tagID") REFERENCES "tags"("id") ON DELETE CASCADE - ) + ) STRICT """ ) .execute(db) @@ -169,58 +169,57 @@ func appDatabase() throws -> any DatabaseWriter { try Reminder.delete().execute(self) try Reminder.insert { Reminder.Draft( - date: Date(), notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", remindersListID: 1, title: "Groceries" ) Reminder.Draft( - date: Date().addingTimeInterval(-60 * 60 * 24 * 2), + dueDate: Date().addingTimeInterval(-60 * 60 * 24 * 2), isFlagged: true, remindersListID: 1, title: "Haircut" ) Reminder.Draft( - date: Date(), + dueDate: Date(), notes: "Ask about diet", priority: .high, remindersListID: 1, title: "Doctor appointment" ) Reminder.Draft( - date: Date().addingTimeInterval(-60 * 60 * 24 * 190), + dueDate: Date().addingTimeInterval(-60 * 60 * 24 * 190), isCompleted: true, remindersListID: 1, title: "Take a walk" ) Reminder.Draft( - date: Date(), + dueDate: Date(), remindersListID: 1, title: "Buy concert tickets" ) Reminder.Draft( - date: Date().addingTimeInterval(60 * 60 * 24 * 2), + dueDate: Date().addingTimeInterval(60 * 60 * 24 * 2), isFlagged: true, priority: .high, remindersListID: 2, title: "Pick up kids from school" ) Reminder.Draft( - date: Date().addingTimeInterval(-60 * 60 * 24 * 2), + dueDate: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, priority: .low, remindersListID: 2, title: "Get laundry" ) Reminder.Draft( - date: Date().addingTimeInterval(60 * 60 * 24 * 4), + dueDate: Date().addingTimeInterval(60 * 60 * 24 * 4), isCompleted: false, priority: .high, remindersListID: 2, title: "Take out trash" ) Reminder.Draft( - date: Date().addingTimeInterval(60 * 60 * 24 * 2), + dueDate: Date().addingTimeInterval(60 * 60 * 24 * 2), notes: """ Status of tax return Expenses for next year @@ -230,7 +229,7 @@ func appDatabase() throws -> any DatabaseWriter { title: "Call accountant" ) Reminder.Draft( - date: Date().addingTimeInterval(-60 * 60 * 24 * 2), + dueDate: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, priority: .medium, remindersListID: 3, diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 8f28fa0b..b56bfa03 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -81,7 +81,7 @@ struct SearchRemindersView: View { Reminder .searching(searchText) .where { showCompletedInSearchResults || !$0.isCompleted } - .order { ($0.isCompleted, $0.date) } + .order { ($0.isCompleted, $0.dueDate) } .withTags .join(RemindersList.all) { $0.remindersListID.eq($3.id) } .select { @@ -103,7 +103,7 @@ struct SearchRemindersView: View { .where(\.isCompleted) .where { if let monthsAgo { - #sql("\($0.date) < date('now', '-\(raw: monthsAgo) months')") + #sql("\($0.dueDate) < date('now', '-\(raw: monthsAgo) months')") } } .delete() diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 59a525c9..1334bb43 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -142,7 +142,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "6da773bfe57eb0a81d1d8271ff09f1c7d0e21764" + "revision" : "a7fa9048310eb0aa6d182a7c0a0f133e0d605c58" } }, { From 39745aa6762012af4cf35d09a6b77ef458f2fe97 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 13 Apr 2025 08:43:49 -0700 Subject: [PATCH 087/171] wip --- Examples/Reminders/Helpers.swift | 35 +++++++++++++++---- Examples/Reminders/ReminderForm.swift | 14 ++++---- Examples/Reminders/ReminderRow.swift | 8 ++--- Examples/Reminders/RemindersListDetail.swift | 17 ++++----- Examples/Reminders/RemindersListForm.swift | 17 ++++----- Examples/Reminders/RemindersListRow.swift | 2 +- Examples/Reminders/Schema.swift | 28 +++++++++++---- Examples/Reminders/SearchReminders.swift | 25 ++++++------- .../xcshareddata/swiftpm/Package.resolved | 6 ++-- 9 files changed, 89 insertions(+), 63 deletions(-) diff --git a/Examples/Reminders/Helpers.swift b/Examples/Reminders/Helpers.swift index 83d12a22..9251856c 100644 --- a/Examples/Reminders/Helpers.swift +++ b/Examples/Reminders/Helpers.swift @@ -1,11 +1,34 @@ +import StructuredQueriesCore import SwiftUI extension Color { - static func hex(_ hex: Int) -> Self { - Color( - red: Double((hex >> 16) & 0xFF) / 255.0, - green: Double((hex >> 8) & 0xFF) / 255.0, - blue: Double(hex & 0xFF) / 255.0 - ) + public struct HexRepresentation: QueryBindable, QueryRepresentable { + public var queryOutput: Color + public var queryBinding: QueryBinding { + guard let components = UIColor(queryOutput).cgColor.components + else { + struct InvalidColor: Error {} + return .invalid(InvalidColor()) + } + let r = Int64(components[0] * 0xFF) << 24 + let g = Int64(components[1] * 0xFF) << 16 + let b = Int64(components[2] * 0xFF) << 8 + let a = Int64((components.indices.contains(3) ? components[3] : 1) * 0xFF) + return .int(r | g | b | a) + } + public init(queryOutput: Color) { + self.queryOutput = queryOutput + } + public init(decoder: inout some QueryDecoder) throws { + let hex = try Int(decoder: &decoder) + self.init( + queryOutput: Color( + red: Double((hex >> 24) & 0xFF) / 0xFF, + green: Double((hex >> 16) & 0xFF) / 0xFF, + blue: Double((hex >> 8) & 0xFF) / 0xFF, + opacity: Double(hex & 0xFF) / 0xFF + ) + ) + } } } diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 0663ee65..61cacab6 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -97,11 +97,11 @@ struct ReminderFormView: View { } } Picker(selection: $reminder.priority) { - Text("None").tag(Int?.none) + Text("None").tag(Priority?.none) Divider() - Text("High").tag(3) - Text("Medium").tag(2) - Text("Low").tag(1) + Text("High").tag(Priority.high) + Text("Medium").tag(Priority.medium) + Text("Low").tag(Priority.low) } label: { HStack { Image(systemName: "exclamationmark.circle.fill") @@ -121,7 +121,7 @@ struct ReminderFormView: View { HStack { Image(systemName: "list.bullet.circle.fill") .font(.title) - .foregroundStyle(Color.hex(remindersList.color)) + .foregroundStyle(remindersList.color) Text("List") } } @@ -135,9 +135,9 @@ struct ReminderFormView: View { else { return } do { selectedTags = try await database.read { db in - try Tag.order(by: \.name) + try Tag.select(\.self) + .order(by: \.name) .join(ReminderTag.all) { $0.id.eq($1.tagID) && $1.reminderID.eq(reminderID) } - .select { tag, _ in tag } .fetchAll(db) } } catch { diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 07781d3b..da7e8ea7 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -4,6 +4,7 @@ import SwiftUI struct ReminderRow: View { let isPastDue: Bool + let notes: String let reminder: Reminder let remindersList: RemindersList let tags: [String] @@ -24,10 +25,6 @@ struct ReminderRow: View { VStack(alignment: .leading) { title(for: reminder) - let notes = reminder.notes - .split(separator: "\n", omittingEmptySubsequences: true) - .prefix(3) - .joined(separator: " ") if !notes.isEmpty { Text(notes) .lineLimit(2) @@ -119,7 +116,7 @@ struct ReminderRow: View { + (reminder.priority == nil ? "" : " ") return (Text(exclamations) - .foregroundStyle(reminder.isCompleted ? .gray : Color.hex(remindersList.color)) + .foregroundStyle(reminder.isCompleted ? .gray : remindersList.color) + Text(reminder.title) .foregroundStyle(reminder.isCompleted ? .gray : .primary)) .font(.title3) @@ -142,6 +139,7 @@ struct ReminderRowPreview: PreviewProvider { List { ReminderRow( isPastDue: false, + notes: reminder.notes.replacingOccurrences(of: "\n", with: " "), reminder: reminder, remindersList: reminderList, tags: ["point-free", "adulting"] diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index b041c2dd..a0a71d7f 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -29,6 +29,7 @@ struct RemindersListDetailView: View { ForEach(reminderStates) { reminderState in ReminderRow( isPastDue: reminderState.isPastDue, + notes: reminderState.notes, reminder: reminderState.reminder, remindersList: reminderState.remindersList, tags: reminderState.tags @@ -189,7 +190,8 @@ struct RemindersListDetailView: View { reminder: $0, remindersList: $3, isPastDue: $0.isPastDue, - commaSeparatedTags: $2.name.groupConcat() + notes: $0.notes.replace("\n", " "), + tags: #sql("\($2.name)").jsonGroupArray(filter: $2.name.isNot(nil)) ) }, animation: .default @@ -202,19 +204,12 @@ struct RemindersListDetailView: View { let reminder: Reminder let remindersList: RemindersList let isPastDue: Bool - let commaSeparatedTags: String? - var tags: [String] { - (commaSeparatedTags ?? "").split(separator: ",").map(String.init) - } + let notes: String + @Column(as: JSONRepresentation<[String]>.self) + let tags: [String] } } -extension Reminder { - static let withTags = group(by: \.id) - .leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) } - .leftJoin(Tag.all) { $1.tagID.eq($2.id) } -} - struct RemindersListDetailPreview: PreviewProvider { static var previews: some View { let remindersList = try! prepareDependencies { diff --git a/Examples/Reminders/RemindersListForm.swift b/Examples/Reminders/RemindersListForm.swift index a221db31..1bd8d2b1 100644 --- a/Examples/Reminders/RemindersListForm.swift +++ b/Examples/Reminders/RemindersListForm.swift @@ -23,7 +23,7 @@ struct RemindersListForm: View { var body: some View { Form { TextField("Name", text: $remindersList.name) - ColorPicker("Color", selection: $remindersList.color.cgColor) + ColorPicker("Color", selection: $remindersList.color) } .toolbar { ToolbarItem { @@ -61,19 +61,20 @@ extension Int { fileprivate var cgColor: CGColor { get { CGColor( - red: Double((self >> 16) & 0xFF) / 255.0, - green: Double((self >> 8) & 0xFF) / 255.0, - blue: Double(self & 0xFF) / 255.0, - alpha: 1 + red: Double((self >> 24) & 0xFF) / 255.0, + green: Double((self >> 16) & 0xFF) / 255.0, + blue: Double((self >> 8) & 0xFF) / 255.0, + alpha: Double(self & 0xFF) / 255.0 ) } set { guard let components = newValue.components else { return } self = - (Int(components[0] * 255) << 16) - | (Int(components[1] * 255) << 8) - | Int(components[2] * 255) + (Int(components[0] * 255) << 24) + | (Int(components[1] * 255) << 16) + | (Int(components[2] * 255) << 8) + | Int(components[3] * 255) } } } diff --git a/Examples/Reminders/RemindersListRow.swift b/Examples/Reminders/RemindersListRow.swift index 30d07fd7..318af422 100644 --- a/Examples/Reminders/RemindersListRow.swift +++ b/Examples/Reminders/RemindersListRow.swift @@ -14,7 +14,7 @@ struct RemindersListRow: View { HStack { Image(systemName: "list.bullet.circle.fill") .font(.title) - .foregroundStyle(Color.hex(remindersList.color)) + .foregroundStyle(remindersList.color) Text(remindersList.name) Spacer() Text("\(reminderCount)") diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 5669bb26..5e2e67f4 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -3,11 +3,13 @@ import GRDB import IssueReporting import SharingGRDB import StructuredQueriesGRDB +import SwiftUI @Table struct RemindersList: Hashable, Identifiable { var id: Int - var color = 0x4a99ef + @Column(as: Color.HexRepresentation.self) + var color: Color = Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255) var name = "" } @@ -22,13 +24,16 @@ struct Reminder: Equatable, Identifiable { var priority: Priority? var remindersListID: Int var title = "" + static let incomplete = Self.where { !$0.isCompleted } static func searching(_ text: String) -> Where { Self.where { $0.title.collate(.nocase).contains(text) - || $0.notes.collate(.nocase).contains(text) + || $0.notes.collate(.nocase).contains(text) } } - static let incomplete = Self.where { !$0.isCompleted } + static let withTags = group(by: \.id) + .leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) } + .leftJoin(Tag.all) { $1.tagID.eq($2.id) } } extension Reminder.TableColumns { var isPastDue: some QueryExpression { @@ -88,7 +93,7 @@ func appDatabase() throws -> any DatabaseWriter { """ CREATE TABLE "remindersLists" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "color" INTEGER NOT NULL DEFAULT \(raw: 0x4a99ef), + "color" INTEGER NOT NULL DEFAULT \(raw: 0x4a99ef00), "name" TEXT NOT NULL ) STRICT """ @@ -158,9 +163,18 @@ func appDatabase() throws -> any DatabaseWriter { func createDebugRemindersLists() throws { try RemindersList.delete().execute(self) try RemindersList.insert { - RemindersList.Draft(color: 0x4a99ef, name: "Personal") - RemindersList.Draft(color: 0xed8935, name: "Family") - RemindersList.Draft(color: 0xb25dd3, name: "Business") + RemindersList.Draft( + color: Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255), + name: "Personal" + ) + RemindersList.Draft( + color: Color(red: 0xed / 255, green: 0x89 / 255, blue: 0x35 / 255), + name: "Family" + ) + RemindersList.Draft( + color: Color(red: 0xb2 / 255, green: 0x5d / 255, blue: 0xd3 / 255), + name: "Business" + ) } .execute(self) } diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index b56bfa03..929bb71d 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -33,14 +33,8 @@ struct SearchRemindersView: View { Text("Clear") } Spacer() - if showCompletedInSearchResults { - Button("Hide") { - showCompletedInSearchResults = false - } - } else { - Button("Show") { - showCompletedInSearchResults = true - } + Button(showCompletedInSearchResults ? "Hide" : "Show") { + showCompletedInSearchResults.toggle() } } } @@ -54,6 +48,7 @@ struct SearchRemindersView: View { ForEach(reminders) { reminder in ReminderRow( isPastDue: reminder.isPastDue, + notes: reminder.notes, reminder: reminder.reminder, remindersList: reminder.remindersList, tags: reminder.tags @@ -86,10 +81,11 @@ struct SearchRemindersView: View { .join(RemindersList.all) { $0.remindersListID.eq($3.id) } .select { ReminderState.Columns( - commaSeparatedTags: $2.name.groupConcat(), isPastDue: $0.isPastDue, + notes: $0.notes.replace("\n", " "), reminder: $0, - remindersList: $3 + remindersList: $3, + tags: #sql("\($2.name)").jsonGroupArray(filter: $2.name.isNot(nil)) ) } return .fetchAll(query, animation: .default) @@ -115,13 +111,12 @@ struct SearchRemindersView: View { @Selection struct ReminderState: Identifiable { var id: Reminder.ID { reminder.id } - let commaSeparatedTags: String? - var isPastDue: Bool + let isPastDue: Bool + let notes: String let reminder: Reminders.Reminder let remindersList: RemindersList - var tags: [String] { - (commaSeparatedTags ?? "").split(separator: ",").map(String.init) - } + @Column(as: JSONRepresentation<[String]>.self) + let tags: [String] } } diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1334bb43..540092ff 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "4e6b6a814675daf2c1973514314283448f95f941", - "version" : "1.9.0" + "revision" : "fee6aa29908a75437506ddcbe7434c460605b7e6", + "version" : "1.9.1" } }, { @@ -142,7 +142,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "a7fa9048310eb0aa6d182a7c0a0f133e0d605c58" + "revision" : "f5c542e532a83824e1d9a5dafa141b22af80c290" } }, { From 08535a88aa9c84b2c42290c954488f0af24d087e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 13 Apr 2025 12:54:28 -0700 Subject: [PATCH 088/171] wip --- Examples/Reminders/ReminderRow.swift | 12 +-- Examples/Reminders/RemindersListDetail.swift | 91 +++++++++++++++----- Examples/Reminders/SearchReminders.swift | 1 + 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index da7e8ea7..8286c69f 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -3,6 +3,7 @@ import SharingGRDB import SwiftUI struct ReminderRow: View { + let color: Color let isPastDue: Bool let notes: String let reminder: Reminder @@ -45,19 +46,19 @@ struct ReminderRow: View { } label: { Image(systemName: "info.circle") } + .tint(color) } } } .buttonStyle(.borderless) .swipeActions { - Button("Delete") { + Button("Delete", role: .destructive) { withErrorReporting { try database.write { db in try Reminder.delete(reminder).execute(db) } } } - .tint(.red) Button(reminder.isFlagged ? "Unflag" : "Flag") { withErrorReporting { try database.write { db in @@ -126,22 +127,23 @@ struct ReminderRow: View { struct ReminderRowPreview: PreviewProvider { static var previews: some View { var reminder: Reminder! - var reminderList: RemindersList! + var remindersList: RemindersList! let _ = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() try $0.defaultDatabase.read { db in reminder = try Reminder.all.fetchOne(db) - reminderList = try RemindersList.all.fetchOne(db)! + remindersList = try RemindersList.all.fetchOne(db)! } } NavigationStack { List { ReminderRow( + color: remindersList.color, isPastDue: false, notes: reminder.notes.replacingOccurrences(of: "\n", with: " "), reminder: reminder, - remindersList: reminderList, + remindersList: remindersList, tags: ["point-free", "adulting"] ) } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index a0a71d7f..fcd2bc7d 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -11,6 +11,8 @@ struct RemindersListDetailView: View { let detailType: DetailType @State var isNewReminderSheetPresented = false + @State var isNavigationTitleVisible = false + @State var navigationTitleHeight: CGFloat = 36 @Dependency(\.defaultDatabase) private var database @@ -26,8 +28,18 @@ struct RemindersListDetailView: View { var body: some View { List { + VStack(alignment: .leading) { + GeometryReader { proxy in + Text(detailType.navigationTitle) + .font(.system(.largeTitle, design: .rounded, weight: .bold)) + .foregroundStyle(detailType.color) + .onAppear { navigationTitleHeight = proxy.size.height } + } + } + .listRowSeparator(.hidden) ForEach(reminderStates) { reminderState in ReminderRow( + color: detailType.color, isPastDue: reminderState.isPastDue, notes: reminderState.notes, reminder: reminderState.reminder, @@ -36,13 +48,12 @@ struct RemindersListDetailView: View { ) } } - .task(id: [ordering, showCompleted] as [AnyHashable]) { - await withErrorReporting { - try await updateQuery() - } + .onScrollGeometryChange(for: Bool.self) { geometry in + geometry.contentOffset.y + geometry.contentInsets.top > navigationTitleHeight + } action: { + isNavigationTitleVisible = $1 } - .navigationTitle(detailType.navigationTitle) - .navigationBarTitleDisplayMode(.large) + .listStyle(.plain) .sheet(isPresented: $isNewReminderSheetPresented) { if let remindersList = detailType.list { NavigationStack { @@ -50,6 +61,20 @@ struct RemindersListDetailView: View { } } } + .task(id: [ordering, showCompleted] as [AnyHashable]) { + await withErrorReporting { + try await updateQuery() + } + } + .toolbar { + ToolbarItem(placement: .principal) { + Text(detailType.navigationTitle) + .font(.headline) + .opacity(isNavigationTitleVisible ? 1 : 0) + .animation(.default.speed(2), value: isNavigationTitleVisible) + } + } + .toolbarTitleDisplayMode(.inline) .toolbar { if detailType.is(\.list) { ToolbarItem(placement: .bottomBar) { @@ -66,30 +91,34 @@ struct RemindersListDetailView: View { } Spacer() } + .tint(detailType.color) } } ToolbarItem(placement: .primaryAction) { Menu { - Menu { - ForEach(Ordering.allCases, id: \.self) { ordering in - Button { - self.ordering = ordering - } label: { - Text(ordering.rawValue) - ordering.icon + Group { + Menu { + ForEach(Ordering.allCases, id: \.self) { ordering in + Button { + self.ordering = ordering + } label: { + Text(ordering.rawValue) + ordering.icon + } } + } label: { + Text("Sort By") + Text(ordering.rawValue) + Image(systemName: "arrow.up.arrow.down") + } + Button { + showCompleted.toggle() + } label: { + Text(showCompleted ? "Hide Completed" : "Show Completed") + Image(systemName: showCompleted ? "eye.slash.fill" : "eye") } - } label: { - Text("Sort By") - Text(ordering.rawValue) - Image(systemName: "arrow.up.arrow.down") - } - Button { - showCompleted.toggle() - } label: { - Text(showCompleted ? "Hide Completed" : "Show Completed") - Image(systemName: showCompleted ? "eye.slash.fill" : "eye") } + .tint(detailType.color) } label: { Image(systemName: "ellipsis.circle") } @@ -151,6 +180,22 @@ struct RemindersListDetailView: View { "Today" } } + var color: Color { + switch self { + case .all: + .black + case .completed: + .gray + case .flagged: + .orange + case .list(let list): + list.color + case .scheduled: + .red + case .today: + .blue + } + } } private func updateQuery() async throws { diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 929bb71d..239fb001 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -47,6 +47,7 @@ struct SearchRemindersView: View { ForEach(reminders) { reminder in ReminderRow( + color: reminder.remindersList.color, isPastDue: reminder.isPastDue, notes: reminder.notes, reminder: reminder.reminder, From 98826dee9d7387bc1cf42e86be0c25ccdb418f18 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 14 Apr 2025 16:31:56 -0700 Subject: [PATCH 089/171] wip --- Examples/Reminders/ReminderForm.swift | 19 +++++---- Examples/Reminders/Schema.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- Sources/SharingGRDB/FetchKeyRequest.swift | 42 +++++++++++++------ 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 61cacab6..690e10b3 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -52,11 +52,13 @@ struct ReminderFormView: View { Text("Tags") .foregroundStyle(.black) Spacer() - tagsDetail - .lineLimit(1) - .truncationMode(.tail) - .font(.callout) - .foregroundStyle(.gray) + if let tagsDetail { + tagsDetail + .lineLimit(1) + .truncationMode(.tail) + .font(.callout) + .foregroundStyle(.gray) + } Image(systemName: "chevron.right") } } @@ -160,9 +162,10 @@ struct ReminderFormView: View { } } - private var tagsDetail: Text { - selectedTags.reduce(Text("")) { result, tag in - result + Text("#\(tag.name) ") + private var tagsDetail: Text? { + guard let tag = selectedTags.first else { return nil } + return selectedTags.dropFirst().reduce(Text("#\(tag)")) { result, tag in + result + Text(" #\(tag.name) ") } } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 5e2e67f4..1dc6aa9b 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -9,7 +9,7 @@ import SwiftUI struct RemindersList: Hashable, Identifiable { var id: Int @Column(as: Color.HexRepresentation.self) - var color: Color = Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255) + var color = Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255) var name = "" } diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 540092ff..817df4eb 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "2dfe1f5cc9897e682ed5b52e8734901c9a06e2ced64484fc2a0742abac1657e7", + "originHash" : "066e79de8de548f7199392d294e5d6291d006a14c210bcdde0a9ee46175f050f", "pins" : [ { "identity" : "combine-schedulers", @@ -142,7 +142,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "f5c542e532a83824e1d9a5dafa141b22af80c290" + "revision" : "300ec5c4c0e356f9bff2ea143455ce320b77ab7b" } }, { diff --git a/Sources/SharingGRDB/FetchKeyRequest.swift b/Sources/SharingGRDB/FetchKeyRequest.swift index f9fe3ce8..580562d1 100644 --- a/Sources/SharingGRDB/FetchKeyRequest.swift +++ b/Sources/SharingGRDB/FetchKeyRequest.swift @@ -2,36 +2,52 @@ import GRDB /// A type that can request a value from a database. /// -/// This type can be used to describe a query to read data from SQLite: +/// This type can be used to describe a transaction to read data from SQLite: /// /// ```swift -/// struct Players: FetchKeyRequest { -/// func fetch(_ db: Database) throws -> [Player] { -/// try Player -/// .where { !$0.isInjured } -/// .order(by: \.name) -/// .limit(10) -/// .fetchAll(db) +/// struct PlayersRequest: FetchKeyRequest { +/// struct Value { +/// let injuredPlayerCount: Int +/// let players: [Player] +/// } +/// +/// func fetch(_ db: Database) throws -> Value { +/// try Value( +/// injuredPlayerCount: Player +/// .where(\.isInjured) +/// .fetchCount(db), +/// players: Player +/// .where { !$0.isInjured } +/// .order(by: \.name) +/// .limit(10) +/// .fetchAll(db) +/// ) /// } /// } /// ``` /// -/// And then can be used with `@SharedReader` and -/// ``Sharing/SharedReaderKey/fetch(_:database:animation:)-rgj4`` to popular state with the query -/// in a SwiftUI view, `@Observable` model, UIKit controller, and more: +/// And then can be used with a `@SharedReader` and +/// ``Sharing/SharedReaderKey/fetch(_:database:animation:)-rgj4`` to popular state in a SwiftUI +/// view, `@Observable` model, UIKit view controller, and more: /// /// ```swift /// struct PlayersView: View { -/// @SharedReader(.fetch(Players())) var players +/// @SharedReader(.fetch(PlayersRequest())) var response /// /// var body: some View { -/// ForEach(players) { player in +/// ForEach(response.players) { player in +/// // ... +/// } +/// Button("View injured players (\(response.injuredPlayerCount))") { /// // ... /// } /// } /// } /// ``` public protocol FetchKeyRequest: Hashable, Sendable { + /// The type associated with the request. associatedtype Value + + /// Fetches a value from a database. func fetch(_ db: Database) throws -> Value } From 05f3d0b4736364116076d977524fd4246864f7e8 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 15 Apr 2025 14:57:15 -0700 Subject: [PATCH 090/171] clean up --- Examples/Reminders/ReminderRow.swift | 12 +++++++----- Examples/Reminders/RemindersListDetail.swift | 4 ++-- Examples/Reminders/RemindersLists.swift | 2 -- Examples/Reminders/Schema.swift | 9 +++++++++ Examples/Reminders/SearchReminders.swift | 4 ++-- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 8286c69f..d47a9f8d 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -103,12 +103,14 @@ struct ReminderRow: View { private var subtitleText: Text { let tagsText = tags.reduce(Text(reminder.dueDate == nil ? "" : " ")) { result, tag in - result - + Text("#\(tag) ") - .foregroundStyle(.gray) - .bold() + result + Text("#\(tag) ") } - return (dueText + tagsText).font(.callout) + return + (dueText + + tagsText + .foregroundStyle(.gray) + .bold()) + .font(.callout) } private func title(for reminder: Reminder) -> some View { diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index fcd2bc7d..b2b62684 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -235,8 +235,8 @@ struct RemindersListDetailView: View { reminder: $0, remindersList: $3, isPastDue: $0.isPastDue, - notes: $0.notes.replace("\n", " "), - tags: #sql("\($2.name)").jsonGroupArray(filter: $2.name.isNot(nil)) + notes: $0.inlineNotes, + tags: $2.jsonNames ) }, animation: .default diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 4101f774..4ee40ae4 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -30,8 +30,6 @@ struct RemindersListsView: View { .select { ReminderListState.Columns( reminderCount: #sql("count(iif(\($1.isCompleted), NULL, \($1.id)))"), -// reminderCount: $1.count(filter: !($1.isCompleted ?? false)), -// reminderCount: $1.count(filter: !$1.isCompleted.ifnull(false)), remindersList: $0 ) }, diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 1dc6aa9b..2d82261c 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -45,6 +45,9 @@ extension Reminder.TableColumns { var isScheduled: some QueryExpression { !isCompleted && dueDate.isNot(nil) } + var inlineNotes: some QueryExpression { + notes.replace("\n", " ") + } } enum Priority: Int, QueryBindable { @@ -59,6 +62,12 @@ struct Tag { var name = "" } +extension Tag?.TableColumns { + var jsonNames: some QueryExpression> { + #sql("\(self.name)").jsonGroupArray(filter: self.name.isNot(nil)) + } +} + @Table("remindersTags") struct ReminderTag { var reminderID: Int diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 239fb001..7a7f865e 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -83,10 +83,10 @@ struct SearchRemindersView: View { .select { ReminderState.Columns( isPastDue: $0.isPastDue, - notes: $0.notes.replace("\n", " "), + notes: $0.inlineNotes, reminder: $0, remindersList: $3, - tags: #sql("\($2.name)").jsonGroupArray(filter: $2.name.isNot(nil)) + tags: $2.jsonNames ) } return .fetchAll(query, animation: .default) From 693a2ea9daa1a26e4b843f11a37947c0f1f56238 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 16 Apr 2025 09:08:29 -0700 Subject: [PATCH 091/171] wip --- Examples/SyncUpTests/SyncUpFormTests.swift | 13 ++++--------- Sources/SharingGRDB/DefaultDatabase.swift | 4 +++- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Examples/SyncUpTests/SyncUpFormTests.swift b/Examples/SyncUpTests/SyncUpFormTests.swift index aab71de9..fb1fffb5 100644 --- a/Examples/SyncUpTests/SyncUpFormTests.swift +++ b/Examples/SyncUpTests/SyncUpFormTests.swift @@ -4,15 +4,14 @@ import Testing @testable import SyncUps -@Suite +@Suite(.dependencies { + $0.defaultDatabase = try! SyncUps.appDatabase() + $0.uuid = .incrementing +}) struct SyncUpFormTests { @Dependency(\.defaultDatabase) var database @Test func saveNew() async throws { - prepareDependencies { - $0.defaultDatabase = try! SyncUps.appDatabase() - $0.uuid = .incrementing - } let draft = SyncUp.Draft(title: "Morning Sync") let model = SyncUpFormModel(syncUp: draft) model.addAttendeeButtonTapped() @@ -32,10 +31,6 @@ struct SyncUpFormTests { } @Test func updateExisting() async throws { - prepareDependencies { - $0.defaultDatabase = try! SyncUps.appDatabase() - $0.uuid = .incrementing - } let existingSyncUp = try await database.read { db in try #require(try SyncUp.all.fetchOne(db)) } diff --git a/Sources/SharingGRDB/DefaultDatabase.swift b/Sources/SharingGRDB/DefaultDatabase.swift index 1618fea2..d0851a2e 100644 --- a/Sources/SharingGRDB/DefaultDatabase.swift +++ b/Sources/SharingGRDB/DefaultDatabase.swift @@ -81,7 +81,9 @@ extension DependencyValues { """ } } - reportIssue(message) + if shouldReportUnimplemented { + reportIssue(message) + } var configuration = Configuration() #if DEBUG configuration.label = .defaultDatabaseLabel From 411545a8caf9f65f2d115038ffd3e457643e04d6 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 16 Apr 2025 08:37:42 -0700 Subject: [PATCH 092/171] wip --- Examples/Reminders/ReminderForm.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 690e10b3..64375f83 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -78,7 +78,6 @@ struct ReminderFormView: View { Text("Date") } } - // TODO: Try `Binding.init?` initializer if let dueDate = reminder.dueDate { DatePicker( "", @@ -164,7 +163,7 @@ struct ReminderFormView: View { private var tagsDetail: Text? { guard let tag = selectedTags.first else { return nil } - return selectedTags.dropFirst().reduce(Text("#\(tag)")) { result, tag in + return selectedTags.dropFirst().reduce(Text("#\(tag.name)")) { result, tag in result + Text(" #\(tag.name) ") } } From b929c8d38edf750bad1612898774bc47957afae8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 16 Apr 2025 11:23:04 -0700 Subject: [PATCH 093/171] format --- Examples/Reminders/Schema.swift | 4 ++-- Examples/SyncUpTests/SyncUpFormTests.swift | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 2d82261c..4e69d2d8 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -28,7 +28,7 @@ struct Reminder: Equatable, Identifiable { static func searching(_ text: String) -> Where { Self.where { $0.title.collate(.nocase).contains(text) - || $0.notes.collate(.nocase).contains(text) + || $0.notes.collate(.nocase).contains(text) } } static let withTags = group(by: \.id) @@ -102,7 +102,7 @@ func appDatabase() throws -> any DatabaseWriter { """ CREATE TABLE "remindersLists" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "color" INTEGER NOT NULL DEFAULT \(raw: 0x4a99ef00), + "color" INTEGER NOT NULL DEFAULT \(raw: 0x4a99_ef00), "name" TEXT NOT NULL ) STRICT """ diff --git a/Examples/SyncUpTests/SyncUpFormTests.swift b/Examples/SyncUpTests/SyncUpFormTests.swift index fb1fffb5..61587bdb 100644 --- a/Examples/SyncUpTests/SyncUpFormTests.swift +++ b/Examples/SyncUpTests/SyncUpFormTests.swift @@ -4,10 +4,12 @@ import Testing @testable import SyncUps -@Suite(.dependencies { - $0.defaultDatabase = try! SyncUps.appDatabase() - $0.uuid = .incrementing -}) +@Suite( + .dependencies { + $0.defaultDatabase = try! SyncUps.appDatabase() + $0.uuid = .incrementing + } +) struct SyncUpFormTests { @Dependency(\.defaultDatabase) var database From f7a54681ac02b972188f703fd14b905642aa3090 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 16 Apr 2025 14:03:55 -0700 Subject: [PATCH 094/171] wip --- Examples/SyncUpTests/SyncUpFormTests.swift | 37 +++++ Examples/SyncUps/App.swift | 3 +- Examples/SyncUps/Schema.swift | 75 ++++----- Package.resolved | 150 ++++++++++++++++++ Package.swift | 18 ++- .../xcshareddata/swiftpm/Package.resolved | 4 +- Sources/SharingGRDB/FetchKey.swift | 1 + .../DefaultDatabase.swift | 3 +- Sources/StructuredQueriesGRDBCore/Seed.swift | 26 +++ Tests/SharingGRDBTests/SharingGRDBTests.swift | 2 + .../MigrationTests.swift | 2 +- 11 files changed, 268 insertions(+), 53 deletions(-) create mode 100644 Package.resolved rename Sources/{SharingGRDB => StructuredQueriesGRDBCore}/DefaultDatabase.swift (96%) create mode 100644 Sources/StructuredQueriesGRDBCore/Seed.swift diff --git a/Examples/SyncUpTests/SyncUpFormTests.swift b/Examples/SyncUpTests/SyncUpFormTests.swift index 61587bdb..7766c447 100644 --- a/Examples/SyncUpTests/SyncUpFormTests.swift +++ b/Examples/SyncUpTests/SyncUpFormTests.swift @@ -1,5 +1,8 @@ import Dependencies import DependenciesTestSupport +import GRDB +import Foundation +import StructuredQueries import Testing @testable import SyncUps @@ -7,6 +10,7 @@ import Testing @Suite( .dependencies { $0.defaultDatabase = try! SyncUps.appDatabase() + try! $0.defaultDatabase.write { try $0.seedSyncUpFormTests() } $0.uuid = .incrementing } ) @@ -54,3 +58,36 @@ struct SyncUpFormTests { #expect(attendees.map(\.name) == ["Blob", "Blobby McBlob"]) } } + +extension Database { + fileprivate func seedSyncUpFormTests() throws { + try seed { + SyncUp(id: 1, seconds: 60, theme: .appOrange, title: "Design") + SyncUp(id: 2, seconds: 60 * 10, theme: .periwinkle, title: "Engineering") + SyncUp(id: 3, seconds: 60 * 30, theme: .poppy, title: "Product") + + for name in ["Blob", "Blob Jr", "Blob Sr", "Blob Esq", "Blob III", "Blob I"] { + Attendee.Draft(name: name, syncUpID: 1) + } + for name in ["Blob", "Blob Jr"] { + Attendee.Draft(name: name, syncUpID: 2) + } + for name in ["Blob Sr", "Blob Jr"] { + Attendee.Draft(name: name, syncUpID: 3) + } + + Meeting.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 7), + syncUpID: 1, + transcript: """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ + exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute \ + irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla \ + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia \ + deserunt mollit anim id est laborum. + """ + ) + } + } +} diff --git a/Examples/SyncUps/App.swift b/Examples/SyncUps/App.swift index f3553f9d..826d63f3 100644 --- a/Examples/SyncUps/App.swift +++ b/Examples/SyncUps/App.swift @@ -1,4 +1,5 @@ import CasePaths +import Dependencies import SharingGRDB import SwiftUI @@ -12,8 +13,6 @@ class AppModel { didSet { bind() } } - @ObservationIgnored - @Dependency(\.continuousClock) var clock @ObservationIgnored @Dependency(\.date.now) var now @ObservationIgnored diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index fd45fefa..2f2271e6 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -141,9 +141,12 @@ func appDatabase() throws -> any DatabaseWriter { ) .execute(db) } - #if DEBUG - migrator.registerMigration("Insert sample data") { db in - try db.insertSampleData() + + #if DEBUG && targetEnvironment(simulator) + if context != .test { + migrator.registerMigration("Seed sample data") { db in + try db.seedSampleData() + } } #endif @@ -152,55 +155,35 @@ func appDatabase() throws -> any DatabaseWriter { return database } -#if DEBUG - extension Database { - func insertSampleData() throws { - let design = try SyncUp - .insert(SyncUp.Draft(seconds: 60, theme: .appOrange, title: "Design")) - .returning(\.self) - .fetchOne(self)! +extension Database { + fileprivate func seedSampleData() throws { + try seed { + SyncUp(id: 1, seconds: 60, theme: .appOrange, title: "Design") + SyncUp(id: 2, seconds: 60 * 10, theme: .periwinkle, title: "Engineering") + SyncUp(id: 3, seconds: 60 * 30, theme: .poppy, title: "Product") for name in ["Blob", "Blob Jr", "Blob Sr", "Blob Esq", "Blob III", "Blob I"] { - try Attendee - .insert(Attendee.Draft(name: name, syncUpID: design.id)) - .execute(self) + Attendee.Draft(name: name, syncUpID: 1) } - try Meeting - .insert( - Meeting.Draft( - date: Date().addingTimeInterval(-60 * 60 * 24 * 7), - syncUpID: design.id, - transcript: """ - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ - exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute \ - irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla \ - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia \ - deserunt mollit anim id est laborum. - """ - ) - ) - .execute(self) - - let engineering = try SyncUp - .insert(SyncUp.Draft(seconds: 60 * 10, theme: .periwinkle, title: "Engineering")) - .returning(\.self) - .fetchOne(self)! for name in ["Blob", "Blob Jr"] { - try Attendee - .insert(Attendee.Draft(name: name, syncUpID: engineering.id)) - .execute(self) + Attendee.Draft(name: name, syncUpID: 2) } - - let product = try SyncUp - .insert(SyncUp.Draft(seconds: 60 * 30, theme: .poppy, title: "Product")) - .returning(\.self) - .fetchOne(self)! for name in ["Blob Sr", "Blob Jr"] { - try Attendee - .insert(Attendee.Draft(name: name, syncUpID: product.id)) - .execute(self) + Attendee.Draft(name: name, syncUpID: 3) } + + Meeting.Draft( + date: Date().addingTimeInterval(-60 * 60 * 24 * 7), + syncUpID: 1, + transcript: """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ + exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute \ + irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla \ + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia \ + deserunt mollit anim id est laborum. + """ + ) } } -#endif +} diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..7c925e35 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,150 @@ +{ + "originHash" : "e8242656a4ecf4e3ec407a26619ffc934f2d8395f9f6514a7dc9601e471b5b5a", + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "5928286acce13def418ec36d05a001a9641086f2", + "version" : "1.0.3" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift", + "state" : { + "revision" : "04e73c26c4ce8218ab85aaf791942bb0b204f330", + "version" : "7.4.1" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "fee6aa29908a75437506ddcbe7434c460605b7e6", + "version" : "1.9.1" + } + }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", + "version" : "1.4.3" + } + }, + { + "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", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "671fa54b279fd73933b4a8b34782ebf6c8869145", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-sharing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-sharing", + "state" : { + "revision" : "732871fabfc6b38fcdff5ad2f7336327dbf78e81", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "1be8144023c367c5de701a6313ed29a3a10bf59b", + "version" : "1.18.3" + } + }, + { + "identity" : "swift-structured-queries", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-structured-queries", + "state" : { + "branch" : "main", + "revision" : "5d1012be01a7baba0b45bf519aa6162af1af3999" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index b627aebf..a82db233 100644 --- a/Package.swift +++ b/Package.swift @@ -30,6 +30,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), +// .package(path: "../swift-structured-queries") ], targets: [ .target( @@ -53,6 +54,7 @@ let package = Package( name: "StructuredQueriesGRDBCore", dependencies: [ .product(name: "GRDB", package: "GRDB.swift"), + .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), .product(name: "StructuredQueriesCore", package: "swift-structured-queries"), ] @@ -71,11 +73,25 @@ let package = Package( .product(name: "DependenciesTestSupport", package: "swift-dependencies"), .product(name: "StructuredQueries", package: "swift-structured-queries"), ] - ), + ) ], swiftLanguageModes: [.v6] ) +let swiftSettings: [SwiftSetting] = [ + .enableUpcomingFeature("MemberImportVisibility"), + // .unsafeFlags([ + // "-Xfrontend", + // "-warn-long-function-bodies=50", + // "-Xfrontend", + // "-warn-long-expression-type-checking=50", + // ]) +] + +for index in package.targets.indices { + package.targets[index].swiftSettings = swiftSettings +} + #if !os(Windows) // Add the documentation compiler plugin if possible package.dependencies.append( diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 817df4eb..72af594b 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "066e79de8de548f7199392d294e5d6291d006a14c210bcdde0a9ee46175f050f", + "originHash" : "607741d3807f2372379c4b00744235775edb7aa67579279300061169b031484c", "pins" : [ { "identity" : "combine-schedulers", @@ -142,7 +142,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "300ec5c4c0e356f9bff2ea143455ce320b77ab7b" + "revision" : "7e525c1cf1c9137b05e5cb18ecfa05a44231a516" } }, { diff --git a/Sources/SharingGRDB/FetchKey.swift b/Sources/SharingGRDB/FetchKey.swift index 8e4db446..073e9d9d 100644 --- a/Sources/SharingGRDB/FetchKey.swift +++ b/Sources/SharingGRDB/FetchKey.swift @@ -2,6 +2,7 @@ import Dependencies import Dispatch import GRDB import Sharing +import StructuredQueriesGRDBCore #if canImport(Combine) @preconcurrency import Combine diff --git a/Sources/SharingGRDB/DefaultDatabase.swift b/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift similarity index 96% rename from Sources/SharingGRDB/DefaultDatabase.swift rename to Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift index d0851a2e..1329bfd1 100644 --- a/Sources/SharingGRDB/DefaultDatabase.swift +++ b/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift @@ -1,4 +1,5 @@ import Dependencies +import GRDB extension DependencyValues { /// The default database used by `fetchAll`, `fetchOne`, and `fetch`. @@ -95,6 +96,6 @@ extension DependencyValues { #if DEBUG extension String { - static let defaultDatabaseLabel = "co.pointfree.SharingGRDB.testValue" + package static let defaultDatabaseLabel = "co.pointfree.SharingGRDB.testValue" } #endif diff --git a/Sources/StructuredQueriesGRDBCore/Seed.swift b/Sources/StructuredQueriesGRDBCore/Seed.swift new file mode 100644 index 00000000..17c7b251 --- /dev/null +++ b/Sources/StructuredQueriesGRDBCore/Seed.swift @@ -0,0 +1,26 @@ +import Dependencies +import GRDB +import StructuredQueriesCore + +extension Database { + public func seed( + @InsertValuesBuilder + _ build: () -> [any StructuredQueriesCore.Table] + ) throws { + func open(_ seed: T) throws { + if let seed = seed as? any TableDraft { + func open(_ seed: Draft) throws { + try Draft.PrimaryTable.insert(seed) + .execute(self) + } + try open(seed) + } else { + try T.insert(seed).execute(self) + } + } + + for seed in build() { + try open(seed) + } + } +} diff --git a/Tests/SharingGRDBTests/SharingGRDBTests.swift b/Tests/SharingGRDBTests/SharingGRDBTests.swift index b4105eee..1c709da2 100644 --- a/Tests/SharingGRDBTests/SharingGRDBTests.swift +++ b/Tests/SharingGRDBTests/SharingGRDBTests.swift @@ -1,8 +1,10 @@ import Dependencies +import DependenciesTestSupport import GRDB import Sharing import SharingGRDB import StructuredQueries +import SwiftUI import Testing @Suite struct GRDBSharingTests { diff --git a/Tests/StructuredQueriesGRDBTests/MigrationTests.swift b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift index 653b00f6..c79a081d 100644 --- a/Tests/StructuredQueriesGRDBTests/MigrationTests.swift +++ b/Tests/StructuredQueriesGRDBTests/MigrationTests.swift @@ -29,7 +29,7 @@ import Testing try #expect(abs(#require(grdbDate).timeIntervalSince1970 - timestamp) < 0.001) let date = try #require(try Model.all.fetchOne(db)).date - try #expect(abs(#require(date).timeIntervalSince1970 - timestamp) < 0.001) + #expect(abs(date.timeIntervalSince1970 - timestamp) < 0.001) } } } From c768b229ae2b2cd0ff0670cf496a6bf50bd84179 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 16 Apr 2025 15:20:22 -0700 Subject: [PATCH 095/171] format --- Tests/SharingGRDBTests/SharingGRDBTests.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/SharingGRDBTests/SharingGRDBTests.swift b/Tests/SharingGRDBTests/SharingGRDBTests.swift index a746f557..ad17ce16 100644 --- a/Tests/SharingGRDBTests/SharingGRDBTests.swift +++ b/Tests/SharingGRDBTests/SharingGRDBTests.swift @@ -110,15 +110,15 @@ extension DatabaseWriter where Self == DatabaseQueue { } var migrator = DatabaseMigrator() migrator.registerMigration("Up") { db in - try #sql( - """ - CREATE TABLE "records" ("id" INTEGER PRIMARY KEY AUTOINCREMENT) - """ - ) - .execute(db) - for index in 1...3 { - _ = try Record(id: index).inserted(db) - } + try #sql( + """ + CREATE TABLE "records" ("id" INTEGER PRIMARY KEY AUTOINCREMENT) + """ + ) + .execute(db) + for index in 1...3 { + _ = try Record(id: index).inserted(db) + } } try migrator.migrate(database) return database From 1652657814814a90fdc5311c46a6486a92d009b4 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 16 Apr 2025 15:30:03 -0700 Subject: [PATCH 096/171] wip --- SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- Sources/StructuredQueriesGRDBCore/Seed.swift | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 72af594b..7ffbd7ee 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "607741d3807f2372379c4b00744235775edb7aa67579279300061169b031484c", + "originHash" : "59d9f2fa68c027a6b1d4eb2df0ed686e16de5eedcc82faf142099882c77be4fc", "pins" : [ { "identity" : "combine-schedulers", @@ -142,7 +142,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "7e525c1cf1c9137b05e5cb18ecfa05a44231a516" + "revision" : "5d1012be01a7baba0b45bf519aa6162af1af3999" } }, { diff --git a/Sources/StructuredQueriesGRDBCore/Seed.swift b/Sources/StructuredQueriesGRDBCore/Seed.swift index 17c7b251..99aabcaa 100644 --- a/Sources/StructuredQueriesGRDBCore/Seed.swift +++ b/Sources/StructuredQueriesGRDBCore/Seed.swift @@ -3,6 +3,7 @@ import GRDB import StructuredQueriesCore extension Database { + // TODO: docs public func seed( @InsertValuesBuilder _ build: () -> [any StructuredQueriesCore.Table] From 659bb817aeda9152760c901fa43c5e5170512061 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 16 Apr 2025 16:37:43 -0700 Subject: [PATCH 097/171] batch seed --- Sources/StructuredQueriesGRDBCore/Seed.swift | 30 +++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/Seed.swift b/Sources/StructuredQueriesGRDBCore/Seed.swift index 99aabcaa..a6cf6385 100644 --- a/Sources/StructuredQueriesGRDBCore/Seed.swift +++ b/Sources/StructuredQueriesGRDBCore/Seed.swift @@ -8,20 +8,28 @@ extension Database { @InsertValuesBuilder _ build: () -> [any StructuredQueriesCore.Table] ) throws { - func open(_ seed: T) throws { - if let seed = seed as? any TableDraft { - func open(_ seed: Draft) throws { - try Draft.PrimaryTable.insert(seed) - .execute(self) + var seeds = build() + while !seeds.isEmpty { + guard let first = seeds.first else { break } + let firstType = type(of: first) + + if let firstType = firstType as? any TableDraft.Type { + func insertBatch(_: T.Type) throws { + let batch = Array(seeds.lazy.prefix { $0 is T }.compactMap { $0 as? T }) + defer { seeds.removeFirst(batch.count) } + try T.PrimaryTable.insert(batch).execute(self) } - try open(seed) + + try insertBatch(firstType) } else { - try T.insert(seed).execute(self) - } - } + func insertBatch(_: T.Type) throws { + let batch = Array(seeds.lazy.prefix { $0 is T }.compactMap { $0 as? T }) + defer { seeds.removeFirst(batch.count) } + try T.insert(batch).execute(self) + } - for seed in build() { - try open(seed) + try insertBatch(firstType) + } } } } From 8179a9a9e04d9298fe5afcf106a02cd4658131c9 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 16 Apr 2025 16:53:28 -0700 Subject: [PATCH 098/171] wip --- Examples/Reminders/Schema.swift | 109 ++++++++----------- Sources/StructuredQueriesGRDBCore/Seed.swift | 47 +++++++- 2 files changed, 94 insertions(+), 62 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 4e69d2d8..776ab8d7 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -151,9 +151,11 @@ func appDatabase() throws -> any DatabaseWriter { ) .execute(db) } - #if DEBUG - migrator.registerMigration("Add mock data") { db in - try db.createMockData() + #if DEBUG && targetEnvironment(simulator) + if context != .test { + migrator.registerMigration("Seed sample data") { db in + try db.seedSampleData() + } } #endif try migrator.migrate(database) @@ -163,85 +165,83 @@ func appDatabase() throws -> any DatabaseWriter { #if DEBUG extension Database { - func createMockData() throws { - try createDebugRemindersLists() - try createDebugReminders() - try createDebugTags() - } - - func createDebugRemindersLists() throws { - try RemindersList.delete().execute(self) - try RemindersList.insert { - RemindersList.Draft( + func seedSampleData() throws { + try seed { + RemindersList( + id: 1, color: Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255), name: "Personal" ) - RemindersList.Draft( + RemindersList( + id: 2, color: Color(red: 0xed / 255, green: 0x89 / 255, blue: 0x35 / 255), name: "Family" ) - RemindersList.Draft( + RemindersList( + id: 3, color: Color(red: 0xb2 / 255, green: 0x5d / 255, blue: 0xd3 / 255), name: "Business" ) - } - .execute(self) - } - - func createDebugReminders() throws { - try Reminder.delete().execute(self) - try Reminder.insert { - Reminder.Draft( + Reminder( + id: 1, notes: "Milk\nEggs\nApples\nOatmeal\nSpinach", remindersListID: 1, title: "Groceries" ) - Reminder.Draft( + Reminder( + id: 2, dueDate: Date().addingTimeInterval(-60 * 60 * 24 * 2), isFlagged: true, remindersListID: 1, title: "Haircut" ) - Reminder.Draft( + Reminder( + id: 3, dueDate: Date(), notes: "Ask about diet", priority: .high, remindersListID: 1, title: "Doctor appointment" ) - Reminder.Draft( + Reminder( + id: 4, dueDate: Date().addingTimeInterval(-60 * 60 * 24 * 190), isCompleted: true, remindersListID: 1, title: "Take a walk" ) - Reminder.Draft( + Reminder( + id: 5, dueDate: Date(), remindersListID: 1, title: "Buy concert tickets" ) - Reminder.Draft( + Reminder( + id: 6, dueDate: Date().addingTimeInterval(60 * 60 * 24 * 2), isFlagged: true, priority: .high, remindersListID: 2, title: "Pick up kids from school" ) - Reminder.Draft( + Reminder( + id: 7, dueDate: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, priority: .low, remindersListID: 2, title: "Get laundry" ) - Reminder.Draft( + Reminder( + id: 8, dueDate: Date().addingTimeInterval(60 * 60 * 24 * 4), isCompleted: false, priority: .high, remindersListID: 2, title: "Take out trash" ) - Reminder.Draft( + Reminder( + id: 9, dueDate: Date().addingTimeInterval(60 * 60 * 24 * 2), notes: """ Status of tax return @@ -251,43 +251,30 @@ func appDatabase() throws -> any DatabaseWriter { remindersListID: 3, title: "Call accountant" ) - Reminder.Draft( + Reminder( + id: 10, dueDate: Date().addingTimeInterval(-60 * 60 * 24 * 2), isCompleted: true, priority: .medium, remindersListID: 3, title: "Send weekly emails" ) + Tag(id: 1, name: "car") + Tag(id: 2, name: "kids") + Tag(id: 3, name: "someday") + Tag(id: 4, name: "optional") + Tag(id: 5, name: "social") + Tag(id: 6, name: "night") + Tag(id: 7, name: "adulting") + ReminderTag(reminderID: 1, tagID: 3) + ReminderTag(reminderID: 1, tagID: 4) + ReminderTag(reminderID: 1, tagID: 7) + ReminderTag(reminderID: 2, tagID: 3) + ReminderTag(reminderID: 2, tagID: 4) + ReminderTag(reminderID: 3, tagID: 7) + ReminderTag(reminderID: 4, tagID: 1) + ReminderTag(reminderID: 4, tagID: 2) } - .execute(self) - } - - func createDebugTags() throws { - try ReminderTag.delete().execute(self) - try Tag.delete().execute(self) - try Tag.insert(\.name) { - "car" - "kids" - "someday" - "optional" - "social" - "night" - "adulting" - } - .execute(self) - try ReminderTag.insert { - ($0.reminderID, $0.tagID) - } values: { - (1, 3) - (1, 4) - (1, 7) - (2, 3) - (2, 4) - (3, 7) - (4, 1) - (4, 2) - } - .execute(self) } } #endif diff --git a/Sources/StructuredQueriesGRDBCore/Seed.swift b/Sources/StructuredQueriesGRDBCore/Seed.swift index a6cf6385..042aacdb 100644 --- a/Sources/StructuredQueriesGRDBCore/Seed.swift +++ b/Sources/StructuredQueriesGRDBCore/Seed.swift @@ -3,7 +3,52 @@ import GRDB import StructuredQueriesCore extension Database { - // TODO: docs + /// Seeds a database with the given values. + /// + /// This function is useful for seeding a database's initial state, especially for previews and + /// tests. You can list out a bunch of table records and drafts and they will be inserted into the + /// database: + /// + /// ```swift + /// try db.seed { + /// SyncUp(id: 1, seconds: 60, theme: .appOrange, title: "Design") + /// SyncUp(id: 2, seconds: 60 * 10, theme: .periwinkle, title: "Engineering") + /// SyncUp(id: 3, seconds: 60 * 30, theme: .poppy, title: "Product") + /// + /// for name in ["Blob", "Blob Jr", "Blob Sr", "Blob Esq", "Blob III", "Blob I"] { + /// Attendee.Draft(name: name, syncUpID: 1) + /// } + /// for name in ["Blob", "Blob Jr"] { + /// Attendee.Draft(name: name, syncUpID: 2) + /// } + /// for name in ["Blob Sr", "Blob Jr"] { + /// Attendee.Draft(name: name, syncUpID: 3) + /// } + /// } + /// // INSERT INTO "syncUps" + /// // ("id", "seconds", "theme", "title") + /// // VALUES + /// // (1, 60, 'appOrange', 'Design'), + /// // (2, 600, 'periwinkle', 'Engineering'), + /// // (3, 1800, 'poppy', 'Product'); + /// // INSERT INTO "attendees" + /// // ("id", "name", "syncUpID") + /// // VALUES + /// // (NULL, 'Blob', 1), + /// // (NULL, 'Blob Jr', 1), + /// // (NULL, 'Blob Sr', 1), + /// // (NULL, 'Blob Esq', 1), + /// // (NULL, 'Blob III', 1), + /// // (NULL, 'Blob I', 1), + /// // (NULL, 'Blob', 2), + /// // (NULL, 'Blob Jr', 2), + /// // (NULL, 'Blob Sr', 3), + /// // (NULL, 'Blob Jr', 3) + /// ``` + /// + /// Insertions are performed in order and batched per table. + /// + /// - Parameter build: A result builder closure that inserts every built row. public func seed( @InsertValuesBuilder _ build: () -> [any StructuredQueriesCore.Table] From d78ac329bd207eb1609e8693f87617473b97f1b7 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 16 Apr 2025 18:51:46 -0700 Subject: [PATCH 099/171] clean up --- Examples/Reminders/RemindersListForm.swift | 46 ++-------------------- Examples/Reminders/RemindersListRow.swift | 6 +-- 2 files changed, 5 insertions(+), 47 deletions(-) diff --git a/Examples/Reminders/RemindersListForm.swift b/Examples/Reminders/RemindersListForm.swift index 1bd8d2b1..a88c9f6d 100644 --- a/Examples/Reminders/RemindersListForm.swift +++ b/Examples/Reminders/RemindersListForm.swift @@ -6,18 +6,11 @@ import SwiftUI struct RemindersListForm: View { @Dependency(\.defaultDatabase) private var database - let remindersListID: RemindersList.ID? @State var remindersList: RemindersList.Draft @Environment(\.dismiss) var dismiss - init(existingList: RemindersList? = nil) { - if let existingList { - remindersListID = existingList.id - remindersList = RemindersList.Draft(color: existingList.color, name: existingList.name) - } else { - remindersListID = nil - remindersList = RemindersList.Draft() - } + init(existingList: RemindersList.Draft? = nil) { + remindersList = existingList ?? RemindersList.Draft() } var body: some View { @@ -30,18 +23,7 @@ struct RemindersListForm: View { Button("Save") { withErrorReporting { try database.write { db in - guard let remindersListID - else { - try RemindersList.insert(remindersList).execute(db) - return - } - try RemindersList.update( - RemindersList( - id: remindersListID, - color: remindersList.color, - name: remindersList.name - ) - ) + try RemindersList.upsert(remindersList) .execute(db) } } @@ -57,28 +39,6 @@ struct RemindersListForm: View { } } -extension Int { - fileprivate var cgColor: CGColor { - get { - CGColor( - red: Double((self >> 24) & 0xFF) / 255.0, - green: Double((self >> 16) & 0xFF) / 255.0, - blue: Double((self >> 8) & 0xFF) / 255.0, - alpha: Double(self & 0xFF) / 255.0 - ) - } - set { - guard let components = newValue.components - else { return } - self = - (Int(components[0] * 255) << 24) - | (Int(components[1] * 255) << 16) - | (Int(components[2] * 255) << 8) - | Int(components[3] * 255) - } - } -} - #Preview { let _ = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() diff --git a/Examples/Reminders/RemindersListRow.swift b/Examples/Reminders/RemindersListRow.swift index 318af422..50250a8e 100644 --- a/Examples/Reminders/RemindersListRow.swift +++ b/Examples/Reminders/RemindersListRow.swift @@ -23,9 +23,7 @@ struct RemindersListRow: View { Button { withErrorReporting { try database.write { db in - try RemindersList - .where { $0.id == remindersList.id } - .delete() + try RemindersList.delete(remindersList) .execute(db) } } @@ -41,7 +39,7 @@ struct RemindersListRow: View { } .sheet(item: $editList) { list in NavigationStack { - RemindersListForm(existingList: list) + RemindersListForm(existingList: RemindersList.Draft(list)) .navigationTitle("Edit list") } .presentationDetents([.medium]) From f6f41e6d3fe430f35f9e1b5f89b1b80b35aeeb6c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 16 Apr 2025 22:59:57 -0700 Subject: [PATCH 100/171] wip --- .spi.yml | 2 ++ Examples/Reminders/RemindersListForm.swift | 2 +- Examples/SyncUpTests/SyncUpFormTests.swift | 2 +- Package.swift | 1 - .../Documentation.docc/StructuredQueriesGRDBCore.md | 4 ++++ Sources/StructuredQueriesGRDBCore/Seed.swift | 4 ++-- 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.spi.yml b/.spi.yml index 818e9152..9dabcfa4 100644 --- a/.spi.yml +++ b/.spi.yml @@ -3,4 +3,6 @@ builder: configs: - documentation_targets: - SharingGRDB + - StructuredQueriesGRDB + - StructuredQueriesGRDBCore swift_version: 6.0 diff --git a/Examples/Reminders/RemindersListForm.swift b/Examples/Reminders/RemindersListForm.swift index a88c9f6d..63b317b6 100644 --- a/Examples/Reminders/RemindersListForm.swift +++ b/Examples/Reminders/RemindersListForm.swift @@ -24,7 +24,7 @@ struct RemindersListForm: View { withErrorReporting { try database.write { db in try RemindersList.upsert(remindersList) - .execute(db) + .execute(db) } } dismiss() diff --git a/Examples/SyncUpTests/SyncUpFormTests.swift b/Examples/SyncUpTests/SyncUpFormTests.swift index 7766c447..0bba708a 100644 --- a/Examples/SyncUpTests/SyncUpFormTests.swift +++ b/Examples/SyncUpTests/SyncUpFormTests.swift @@ -1,7 +1,7 @@ import Dependencies import DependenciesTestSupport -import GRDB import Foundation +import GRDB import StructuredQueries import Testing diff --git a/Package.swift b/Package.swift index 0a0824e4..4ec8fb0e 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,6 @@ let package = Package( .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), -// .package(path: "../swift-structured-queries") ], targets: [ .target( diff --git a/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md index b3e09eab..4e36709d 100644 --- a/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md +++ b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md @@ -64,3 +64,7 @@ For more information on how to build queries, see the [Structured Queries docume ### Iterating over rows - ``QueryCursor`` + +### Seeding data + +- ``GRDB/Database/seed(_:)`` diff --git a/Sources/StructuredQueriesGRDBCore/Seed.swift b/Sources/StructuredQueriesGRDBCore/Seed.swift index 042aacdb..271495a7 100644 --- a/Sources/StructuredQueriesGRDBCore/Seed.swift +++ b/Sources/StructuredQueriesGRDBCore/Seed.swift @@ -43,10 +43,10 @@ extension Database { /// // (NULL, 'Blob', 2), /// // (NULL, 'Blob Jr', 2), /// // (NULL, 'Blob Sr', 3), - /// // (NULL, 'Blob Jr', 3) + /// // (NULL, 'Blob Jr', 3); /// ``` /// - /// Insertions are performed in order and batched per table. + /// Insertions are performed in order and in batches of consecutive records of the same table. /// /// - Parameter build: A result builder closure that inserts every built row. public func seed( From 61979b958c33b9c364a699794e989ca6494fe52e Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 17 Apr 2025 10:05:36 -0700 Subject: [PATCH 101/171] wip --- Examples/Reminders/ReminderForm.swift | 16 +++------ Examples/Reminders/ReminderRow.swift | 4 +-- Package.resolved | 4 +-- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../QueryCursor.swift | 12 +++---- .../QueryCursorTests.swift | 34 +++++++++++++++++++ 6 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 Tests/StructuredQueriesGRDBTests/QueryCursorTests.swift diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 64375f83..e945724f 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -171,18 +171,10 @@ struct ReminderFormView: View { private func saveButtonTapped() { withErrorReporting { try database.write { db in - guard let reminderID = try Reminder.upsert(reminder).returning(\.id).fetchOne(db) - else { - reportIssue("Could not upsert reminder") - return - } - if reminder.id != nil { - try ReminderTag.where { $0.reminderID == reminderID } - .delete() - .execute(db) - } - guard !selectedTags.isEmpty - else { return } + let reminderID = try Reminder.upsert(reminder).returning(\.id).fetchOne(db)! + try ReminderTag.where { $0.reminderID.eq(reminderID) } + .delete() + .execute(db) try ReminderTag.insert( selectedTags.map { tag in ReminderTag(reminderID: reminderID, tagID: tag.id) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index d47a9f8d..30483915 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -63,7 +63,7 @@ struct ReminderRow: View { withErrorReporting { try database.write { db in try Reminder - .where { $0.id.eq(reminder.id) } + .find(reminder.id) .update { $0.isFlagged.toggle() } .execute(db) } @@ -85,7 +85,7 @@ struct ReminderRow: View { withErrorReporting { try database.write { db in try Reminder - .where { $0.id.eq(reminder.id) } + .find(reminder.id) .update { $0.isCompleted.toggle() } .execute(db) } diff --git a/Package.resolved b/Package.resolved index 7c925e35..3514feb3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e8242656a4ecf4e3ec407a26619ffc934f2d8395f9f6514a7dc9601e471b5b5a", + "originHash" : "ccc2c1d01b329a42bbfac790e28391eb1352919add9af7ad0dce67fa9f5ef18a", "pins" : [ { "identity" : "combine-schedulers", @@ -124,7 +124,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "5d1012be01a7baba0b45bf519aa6162af1af3999" + "revision" : "eaf60f59cac4074abee164048ada8e4920789e26" } }, { diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7ffbd7ee..3026515b 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -142,7 +142,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "5d1012be01a7baba0b45bf519aa6162af1af3999" + "revision" : "7f488e1c2169b3bbaaa2a88db2e4db86a8709672" } }, { diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index e3ef1f9c..6ad01c07 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -65,17 +65,13 @@ final class QueryVoidCursor: QueryCursor { } } -@usableFromInline -struct EmptyQuery: Error { - @usableFromInline - init() {} -} - extension Database { @inlinable func prepare(query: QueryFragment) throws -> (GRDB.Statement, SQLiteQueryDecoder) { - guard !query.isEmpty else { throw EmptyQuery() } - let statement = try makeStatement(sql: query.string) + let queryString = query.isEmpty + ? "SELECT 1 WHERE 0 -- Empty query generated by Structured Queries" + : query.string + let statement = try makeStatement(sql: queryString) statement.arguments = try StatementArguments(query.bindings.map { try $0.databaseValue }) return ( statement, diff --git a/Tests/StructuredQueriesGRDBTests/QueryCursorTests.swift b/Tests/StructuredQueriesGRDBTests/QueryCursorTests.swift new file mode 100644 index 00000000..dd31d774 --- /dev/null +++ b/Tests/StructuredQueriesGRDBTests/QueryCursorTests.swift @@ -0,0 +1,34 @@ +import GRDB +import StructuredQueriesGRDB +import Testing + +@Suite struct QueryCursorTests { + let database: DatabaseQueue + init() throws { + var configuration = Configuration() + configuration.prepareDatabase { + $0.trace { print($0) } + } + database = try DatabaseQueue(configuration: configuration) + try database.write { db in + try #sql(#"CREATE TABLE "numbers" ("value" INTEGER NOT NULL)"#) + .execute(db) + } + } + + @Test func emptyInsert() throws { + try database.write { db in + try Number.insert([]).execute(db) + } + } + + @Test func emptyUpdate() throws { + try database.write { db in + try Number.update { _ in }.execute(db) + } + } +} + +@Table private struct Number { + var value = 0 +} From 1884ae4b3fdcdc4c26be654a1213f37e1fd2a929 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Apr 2025 10:18:15 -0700 Subject: [PATCH 102/171] wip --- .../Articles/ComparisonWithSwiftData.md | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md index 68c10f69..c2119406 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -458,14 +458,16 @@ Lightweight migrations in SwiftData work for simple situations, such as adding a } migrator.registerMigration("Create 'items' table") { db in - try #sql(""" + try #sql( + """ CREATE TABLE "items" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "title" TEXT NOT NULL, "isInStock" INTEGER NOT NULL DEFAULT 1 ) - """) - .execute(db) + """ + ) + .execute(db) } ``` } @@ -499,10 +501,12 @@ adding a `description` field to the `Item` type: } migrator.registerMigration("Add 'description' column to 'items'") { db in - try #sql(""" + try #sql( + """ ALTER TABLE "items" ADD COLUMN "description" TEXT - """) - .execute(db) + """ + ) + .execute(db) } ``` } @@ -575,10 +579,12 @@ structure of your data types. The overall steps to follow are as such: } .execute() // 2️⃣ Create unique index - try #sql(""" + try #sql( + """ CREATE UNIQUE INDEX "items_title" ON "items" ("title") - """) - .execute(db) + """ + ) + .execute(db) } ``` } From 43603fadd2bfa5741a43f6eaa8a0aa64f11edfd2 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Apr 2025 14:16:07 -0700 Subject: [PATCH 103/171] wip --- Examples/CaseStudies/Animations.swift | 10 +++------- Examples/CaseStudies/DynamicQuery.swift | 11 ++++------- .../CaseStudies/ObservableModelDemo.swift | 19 +++++++------------ Examples/CaseStudies/SwiftUIDemo.swift | 8 ++------ Examples/CaseStudies/TransactionDemo.swift | 8 ++------ Examples/CaseStudies/UIKitDemo.swift | 8 ++------ Examples/Reminders/ReminderForm.swift | 3 ++- Examples/SyncUps/SyncUpForm.swift | 13 ++++--------- .../Documentation.docc/Articles/Fetching.md | 2 +- 9 files changed, 27 insertions(+), 55 deletions(-) diff --git a/Examples/CaseStudies/Animations.swift b/Examples/CaseStudies/Animations.swift index c6d6ef39..15960d43 100644 --- a/Examples/CaseStudies/Animations.swift +++ b/Examples/CaseStudies/Animations.swift @@ -37,12 +37,8 @@ struct AnimationsCaseStudy: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - try Fact.insert { - $0.body - } values: { - fact - } - .execute(db) + try Fact.insert(Fact.Draft(body: fact)) + .execute(db) } } } catch {} @@ -52,7 +48,7 @@ struct AnimationsCaseStudy: SwiftUICaseStudy { @Table private struct Fact: Identifiable { - let id: Int64 + let id: Int var body: String } diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index 38408481..20289b56 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -42,8 +42,9 @@ struct DynamicQueryDemo: SwiftUICaseStudy { .onDelete { indexSet in withErrorReporting { try database.write { db in + let ids = indexSet.map { facts.facts[$0].id } try Fact - .where { $0.id.in(indexSet.compactMap { facts.facts[$0].id }) } + .where { $0.id.in(ids) } .delete() .execute(db) } @@ -69,12 +70,8 @@ struct DynamicQueryDemo: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - try Fact.insert { - $0.body - } values: { - fact - } - .execute(db) + try Fact.insert(Fact.Draft(body: fact)) + .execute(db) } } } catch {} diff --git a/Examples/CaseStudies/ObservableModelDemo.swift b/Examples/CaseStudies/ObservableModelDemo.swift index 2a4f2426..1af02aa9 100644 --- a/Examples/CaseStudies/ObservableModelDemo.swift +++ b/Examples/CaseStudies/ObservableModelDemo.swift @@ -67,12 +67,8 @@ private class Model { as: UTF8.self ) try await database.write { db in - try Fact.insert { - $0.body - } values: { - fact - } - .execute(db) + try Fact.insert(Fact.Draft(body: fact)) + .execute(db) } } } @@ -80,12 +76,11 @@ private class Model { func deleteFact(indices: IndexSet) { withErrorReporting { try database.write { db in - try database.write { db in - try Fact - .where { $0.id.in(indices.compactMap { facts[$0].id }) } - .delete() - .execute(db) - } + let ids = indices.map { facts[$0].id } + try Fact + .where { $0.id.in(ids) } + .delete() + .execute(db) } } } diff --git a/Examples/CaseStudies/SwiftUIDemo.swift b/Examples/CaseStudies/SwiftUIDemo.swift index 2f30a649..6ebeb7b4 100644 --- a/Examples/CaseStudies/SwiftUIDemo.swift +++ b/Examples/CaseStudies/SwiftUIDemo.swift @@ -46,12 +46,8 @@ struct SwiftUIDemo: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - try Fact.insert { - $0.body - } values: { - fact - } - .execute(db) + try Fact.insert(Fact.Draft(body: fact)) + .execute(db) } } } catch {} diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index cd305667..eca1e5c8 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -47,12 +47,8 @@ struct TransactionDemo: SwiftUICaseStudy { as: UTF8.self ) try await database.write { db in - try Fact.insert { - $0.body - } values: { - fact - } - .execute(db) + try Fact.insert(Fact.Draft(body: fact)) + .execute(db) } } } catch {} diff --git a/Examples/CaseStudies/UIKitDemo.swift b/Examples/CaseStudies/UIKitDemo.swift index b9edc1ad..d0a38664 100644 --- a/Examples/CaseStudies/UIKitDemo.swift +++ b/Examples/CaseStudies/UIKitDemo.swift @@ -97,12 +97,8 @@ final class UIKitCaseStudyViewController: UICollectionViewController, UIKitCaseS if let fact { await withErrorReporting { try await database.write { db in - try Fact.insert { - $0.body - } values: { - fact - } - .execute(db) + try Fact.insert(Fact.Draft(body: fact)) + .execute(db) } } } diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index e945724f..8c57bf8a 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -138,7 +138,8 @@ struct ReminderFormView: View { selectedTags = try await database.read { db in try Tag.select(\.self) .order(by: \.name) - .join(ReminderTag.all) { $0.id.eq($1.tagID) && $1.reminderID.eq(reminderID) } + .join(ReminderTag.all) { $0.id.eq($1.tagID) } + .where { $1.reminderID.eq(reminderID) } .fetchAll(db) } } catch { diff --git a/Examples/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUpForm.swift index 8a9f64dc..bd186849 100644 --- a/Examples/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUpForm.swift @@ -77,16 +77,11 @@ final class SyncUpFormModel: Identifiable { } withErrorReporting { try database.write { db in - guard let syncUpID = try SyncUp.upsert(syncUp).returning(\.id).fetchOne(db) - else { - reportIssue("Could not upsert sync-up.") - return - } + let syncUpID = try SyncUp.upsert(syncUp).returning(\.id).fetchOne(db)! try Attendee.where { $0.syncUpID == syncUpID }.delete().execute(db) - for attendee in attendees { - try Attendee.insert(Attendee.Draft(name: attendee.name, syncUpID: syncUpID)) - .execute(db) - } + try Attendee + .insert(attendees.map { Attendee.Draft(name: $0.name, syncUpID: syncUpID) }) + .execute(db) } } isDismissed = true diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md index 30e6ca5e..920b7e56 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md @@ -77,7 +77,7 @@ The choice is up to you for each query or query fragment. To learn more, see the [Structured Queries documentation][structured-queries-docs]. [structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries -[structured-queries-docs]: #TODO +[structured-queries-docs]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/ ### Querying with raw SQL From 8761d1bd9ffc2ed827b498d5ff4ad63e69517161 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Apr 2025 14:16:26 -0700 Subject: [PATCH 104/171] wip --- Examples/CaseStudies/TransactionDemo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index eca1e5c8..9bec74f4 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -72,7 +72,7 @@ struct TransactionDemo: SwiftUICaseStudy { @Table private struct Fact: Identifiable { static let databaseTableName = "facts" - let id: Int64 + let id: Int var body: String } From e573ee3c68059a475928630afe705eee6861d0c2 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Apr 2025 14:17:37 -0700 Subject: [PATCH 105/171] S Q -> SQ --- .../Documentation.docc/Articles/Fetching.md | 16 ++++++++-------- .../StructuredQueries/StatementKey.swift | 12 ++++++------ .../Documentation.docc/StructuredQueriesGRDB.md | 2 +- .../StructuredQueriesGRDBCore.md | 6 +++--- .../StructuredQueriesGRDBCore/QueryCursor.swift | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md index 920b7e56..d1cbdd34 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md @@ -6,19 +6,19 @@ Learn about the various tools for fetching data from a SQLite database. All data fetching happens by providing the `fetchAll`, `fetchOne`, or `fetch` key to the `@SharedReader` property wrapper. The primary differences between these choices is whether you want -to build queries with [Structured Queries][structured-queries-gh], specify your query as a raw SQL +to build queries with [StructuredQueries][structured-queries-gh], specify your query as a raw SQL string, or if you want to assemble your value from one or more queries using a raw database connection. - * [Querying with Structured Queries](#Querying-with-Structured-Queries) + * [Querying with StructuredQueries](#Querying-with-Structured-Queries) * [Querying with SQL](#Querying-with-SQL) * [Querying with custom request](#Querying-with-a-custom-request) [structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries -### Querying with Structured Queries +### Querying with StructuredQueries -[Structured Queries][structured-queries-gh] is a library for building type-safe queries that safely +[StructuredQueries][structured-queries-gh] is a library for building type-safe queries that safely and performantly decode into Swift data types. For example, if you simply want to fetch all records from a table, you can do so by plugging the query directly into [`fetchAll`](): @@ -46,9 +46,9 @@ you can do so using the var itemsCount = 0 ``` -While Structured Queries' builder is powerful, it is also stricter than SQLite, which will happily +While StructuredQueries' builder is powerful, it is also stricter than SQLite, which will happily coerce any data into any type, and some queries are more conveniently expressed through these -coercions. Structured Queries should never get in your way, so rather than describe to the Swift +coercions. StructuredQueries should never get in your way, so rather than describe to the Swift type system every explicit cast and coalesce, you can always embed SQL directly in a query using the `#sql` macro: @@ -74,7 +74,7 @@ var items: [Item] ``` The choice is up to you for each query or query fragment. To learn more, see the -[Structured Queries documentation][structured-queries-docs]. +[StructuredQueries documentation][structured-queries-docs]. [structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries [structured-queries-docs]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/ @@ -101,7 +101,7 @@ var itemsCount = 0 These APIs simply feed their data directly to GRDB's equivalent `Database` APIs, which means it is up to you to safely bind arguments and avoid SQL injection. If you want to write SQL queries by -hand, consider using Structured Queries' `#sql` macro, instead. +hand, consider using StructuredQueries' `#sql` macro, instead. [structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift index 3f0ce730..904c4a39 100644 --- a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDB/StructuredQueries/StatementKey.swift @@ -14,7 +14,7 @@ import StructuredQueriesGRDBCore extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// This key takes a query built using the Structured Queries library. + /// This key takes a query built using the StructuredQueries library. /// /// ```swift /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items @@ -40,7 +40,7 @@ extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// This key takes a query built using the Structured Queries library. + /// This key takes a query built using the StructuredQueries library. /// /// ```swift /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items @@ -61,7 +61,7 @@ extension SharedReaderKey { /// A key that can query for a value in a SQLite database. /// - /// This key takes a query built using the Structured Queries library. + /// This key takes a query built using the StructuredQueries library. /// /// ```swift /// @SharedReader(.fetchOne(Item.count())) var itemCount = 0 @@ -87,7 +87,7 @@ extension SharedReaderKey { extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// This key takes a query built using the Structured Queries library. + /// This key takes a query built using the StructuredQueries library. /// /// ```swift /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items @@ -114,7 +114,7 @@ extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// This key takes a query built using the Structured Queries library. + /// This key takes a query built using the StructuredQueries library. /// /// ```swift /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items @@ -144,7 +144,7 @@ extension SharedReaderKey { /// A key that can query for a value in a SQLite database. /// - /// This key takes a query built using the Structured Queries library. + /// This key takes a query built using the StructuredQueries library. /// /// ```swift /// @SharedReader(.fetchOne(Item.count())) var itemCount = 0 diff --git a/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md b/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md index 4640cdc6..e6ad5405 100644 --- a/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md +++ b/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md @@ -1,6 +1,6 @@ # ``StructuredQueriesGRDB`` -A library interfacing Structured Queries with GRDB. +A library interfacing StructuredQueries with GRDB. ## Overview diff --git a/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md index 4e36709d..b947a67a 100644 --- a/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md +++ b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md @@ -1,11 +1,11 @@ # ``StructuredQueriesGRDBCore`` -The core functionality of interfacing Structured Queries with GRDB. This module is automatically +The core functionality of interfacing StructuredQueries with GRDB. This module is automatically imported when you `import StructuredQueriesGRDB`. ## Overview -This library can be used to directly execute queries built using the [Structured Queries][sq-gh] +This library can be used to directly execute queries built using the [StructuredQueries][sq-gh] library and a [GRDB][grdb-gh] database. While the `SharingGRDB` module provides tools to observe queries using the `@SharedReader` property @@ -45,7 +45,7 @@ let averageScore = try Player // SELECT avg("players"."score") FROM "players" ``` -For more information on how to build queries, see the [Structured Queries documentation][sq-spi]. +For more information on how to build queries, see the [StructuredQueries documentation][sq-spi]. [sq-gh]: https://github.com/pointfreeco/swift-structured-queries [sq-spi]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueries diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index 6ad01c07..a79a1978 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -69,7 +69,7 @@ extension Database { @inlinable func prepare(query: QueryFragment) throws -> (GRDB.Statement, SQLiteQueryDecoder) { let queryString = query.isEmpty - ? "SELECT 1 WHERE 0 -- Empty query generated by Structured Queries" + ? "SELECT 1 WHERE 0 -- Empty query generated by StructuredQueries" : query.string let statement = try makeStatement(sql: queryString) statement.arguments = try StatementArguments(query.bindings.map { try $0.databaseValue }) From d650901c44a4c0214fbf95cad3e7dacf663a3e59 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 17 Apr 2025 14:55:41 -0700 Subject: [PATCH 106/171] rename name to title --- Examples/Reminders/ReminderForm.swift | 12 ++++---- Examples/Reminders/RemindersListDetail.swift | 2 +- Examples/Reminders/RemindersListForm.swift | 2 +- Examples/Reminders/RemindersListRow.swift | 4 +-- Examples/Reminders/Schema.swift | 30 ++++++++++---------- Examples/Reminders/TagsForm.swift | 4 +-- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 8c57bf8a..6db36308 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -6,7 +6,7 @@ import StructuredQueriesGRDB import SwiftUI struct ReminderFormView: View { - @SharedReader(.fetchAll(RemindersList.order(by: \.name))) var remindersLists + @SharedReader(.fetchAll(RemindersList.order(by: \.title))) var remindersLists @State var isPresentingTagsPopover = false @State var remindersList: RemindersList @@ -114,7 +114,7 @@ struct ReminderFormView: View { Picker(selection: $remindersList) { ForEach(remindersLists) { remindersList in - Text(remindersList.name) + Text(remindersList.title) .tag(remindersList) .buttonStyle(.plain) } @@ -137,7 +137,7 @@ struct ReminderFormView: View { do { selectedTags = try await database.read { db in try Tag.select(\.self) - .order(by: \.name) + .order(by: \.title) .join(ReminderTag.all) { $0.id.eq($1.tagID) } .where { $1.reminderID.eq(reminderID) } .fetchAll(db) @@ -147,7 +147,7 @@ struct ReminderFormView: View { reportIssue(error) } } - .navigationTitle(remindersList.name) + .navigationTitle(remindersList.title) .toolbar { ToolbarItem { Button(action: saveButtonTapped) { @@ -164,8 +164,8 @@ struct ReminderFormView: View { private var tagsDetail: Text? { guard let tag = selectedTags.first else { return nil } - return selectedTags.dropFirst().reduce(Text("#\(tag.name)")) { result, tag in - result + Text(" #\(tag.name) ") + return selectedTags.dropFirst().reduce(Text("#\(tag.title)")) { result, tag in + result + Text(" #\(tag.title) ") } } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index b2b62684..91026c58 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -173,7 +173,7 @@ struct RemindersListDetailView: View { case .flagged: "Flagged" case .list(let list): - list.name + list.title case .scheduled: "Scheduled" case .today: diff --git a/Examples/Reminders/RemindersListForm.swift b/Examples/Reminders/RemindersListForm.swift index 63b317b6..69e69338 100644 --- a/Examples/Reminders/RemindersListForm.swift +++ b/Examples/Reminders/RemindersListForm.swift @@ -15,7 +15,7 @@ struct RemindersListForm: View { var body: some View { Form { - TextField("Name", text: $remindersList.name) + TextField("Name", text: $remindersList.title) ColorPicker("Color", selection: $remindersList.color) } .toolbar { diff --git a/Examples/Reminders/RemindersListRow.swift b/Examples/Reminders/RemindersListRow.swift index 50250a8e..4ecb3b9b 100644 --- a/Examples/Reminders/RemindersListRow.swift +++ b/Examples/Reminders/RemindersListRow.swift @@ -15,7 +15,7 @@ struct RemindersListRow: View { Image(systemName: "list.bullet.circle.fill") .font(.title) .foregroundStyle(remindersList.color) - Text(remindersList.name) + Text(remindersList.title) Spacer() Text("\(reminderCount)") } @@ -54,7 +54,7 @@ struct RemindersListRow: View { reminderCount: 10, remindersList: RemindersList( id: 1, - name: "Personal" + title: "Personal" ) ) } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 776ab8d7..bcae27cc 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -10,7 +10,7 @@ struct RemindersList: Hashable, Identifiable { var id: Int @Column(as: Color.HexRepresentation.self) var color = Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255) - var name = "" + var title = "" } @Table @@ -59,12 +59,12 @@ enum Priority: Int, QueryBindable { @Table struct Tag { var id: Int - var name = "" + var title = "" } extension Tag?.TableColumns { var jsonNames: some QueryExpression> { - #sql("\(self.name)").jsonGroupArray(filter: self.name.isNot(nil)) + #sql("\(self.title)").jsonGroupArray(filter: self.title.isNot(nil)) } } @@ -103,7 +103,7 @@ func appDatabase() throws -> any DatabaseWriter { CREATE TABLE "remindersLists" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "color" INTEGER NOT NULL DEFAULT \(raw: 0x4a99_ef00), - "name" TEXT NOT NULL + "title" TEXT NOT NULL ) STRICT """ ) @@ -133,7 +133,7 @@ func appDatabase() throws -> any DatabaseWriter { """ CREATE TABLE "tags" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "name" TEXT NOT NULL COLLATE NOCASE UNIQUE + "title" TEXT NOT NULL COLLATE NOCASE UNIQUE ) STRICT """ ) @@ -170,17 +170,17 @@ func appDatabase() throws -> any DatabaseWriter { RemindersList( id: 1, color: Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255), - name: "Personal" + title: "Personal" ) RemindersList( id: 2, color: Color(red: 0xed / 255, green: 0x89 / 255, blue: 0x35 / 255), - name: "Family" + title: "Family" ) RemindersList( id: 3, color: Color(red: 0xb2 / 255, green: 0x5d / 255, blue: 0xd3 / 255), - name: "Business" + title: "Business" ) Reminder( id: 1, @@ -259,13 +259,13 @@ func appDatabase() throws -> any DatabaseWriter { remindersListID: 3, title: "Send weekly emails" ) - Tag(id: 1, name: "car") - Tag(id: 2, name: "kids") - Tag(id: 3, name: "someday") - Tag(id: 4, name: "optional") - Tag(id: 5, name: "social") - Tag(id: 6, name: "night") - Tag(id: 7, name: "adulting") + Tag(id: 1, title: "car") + Tag(id: 2, title: "kids") + Tag(id: 3, title: "someday") + Tag(id: 4, title: "optional") + Tag(id: 5, title: "social") + Tag(id: 6, title: "night") + Tag(id: 7, title: "adulting") ReminderTag(reminderID: 1, tagID: 3) ReminderTag(reminderID: 1, tagID: 4) ReminderTag(reminderID: 1, tagID: 7) diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index ffc87f1d..bd85bec4 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -51,7 +51,7 @@ struct TagsView: View { .join(ReminderTag.all) { $0.id.eq($1.tagID) } .join(Reminder.all) { $1.reminderID.eq($2.id) } .having { $2.count().gt(0) } - .order { ($2.count().desc(), $0.name) } + .order { ($2.count().desc(), $0.title) } .limit(3) .select { tags, _, _ in tags } .fetchAll(db) @@ -87,7 +87,7 @@ private struct TagView: View { if isSelected { Image.init(systemName: "checkmark") } - Text(tag.name) + Text(tag.title) } } .tint(isSelected ? .blue : .black) From 4b31f41d174ea89cad5dc7b585b0b459f5fd7382 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 17 Apr 2025 15:57:49 -0700 Subject: [PATCH 107/171] docs --- .../Articles/ComparisonWithSwiftData.md | 85 +++++++++++++++--- .../Documentation.docc/Articles/Fetching.md | 90 +++++++++++-------- Sources/SharingGRDB/FetchKey.swift | 15 ++++ 3 files changed, 142 insertions(+), 48 deletions(-) diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md index c2119406..2e6cbe36 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -21,6 +21,57 @@ associations, and more. * [Manual migrations](#Manual-migrations) * [Supported Apple platforms](#Supported-Apple-platforms) +### Designing your schema + +Both SharingGRDB and SwiftData come with tools to expose your data types' fields to the compiler +so that type-safe and schema-safe queries can be written. SharingGRDB uses another library of ours +to provide these tools, called [StructuredQueries][sq-gh], and its `@Table` macro works similarly +to SwiftData's `@Model` macro: + +[sq-gh]: http://github.com/pointfreeco/swift-structured-queries + +@Row { + @Column { + ```swift + // SharingGRDB + @Table + struct Item { + let id: Int + var title = "" + var isInStock = true + var notes = "" + } + ``` + } + @Column { + ```swift + // SwiftData + @Model + class Item { + var title: String + var isInStock: Bool + var notes: String + init( + title: String = "", + isInStock: Bool = true, + notes: String = "" + ) { + self.title = title + self.isInStock = isInStock + self.notes = notes + } + } + ``` + } +} + +Some key differences: + +* The `@Table` macro works with struct data types, whereas `@Model` only works with classes. +* Because the `@Model` version of `Item` is a class it is necessary to provide an initializer. +* The `@Model` version of `Item` does not need an `id` field because SwiftData provides a +`persistentIdentifier` to each model. + ### Setting up external storage Both SharingGRDB and SwiftData require some work to be done at the entry point of the app in order @@ -108,9 +159,8 @@ whereas you use the `@Query` macro with SwiftData: } The `@SharedReader` property wrapper takes a variety of options, detailed more in , -and allows you to write raw SQL queries for fetching and aggregating data from your database. It -is also possibly to construct SQL queries using SharingGRDB's query builder syntax. See - [`fetchAll`]() for more information. +and allows you to write queries using a type-safe and schema-safe builder syntax, or you can write +safe SQL strings that are schema-safe and protect you from SQL injection. ### Fetching data for an @Observable model @@ -398,13 +448,14 @@ for sport in sports { This is powerful, but it can also lead to a number of problems in apps. First, the only way for this mechanism to work is for `Team` and `Sport` to be classes, and the `@Model` macro enforces that. Second, because the SQLite execution is so abstracted from us, it makes it easy to execute many, -_many_ queries, leading to inefficient code. In this case, we are executing a whole new SQL query -for each sport in order to get their teams. And on top of that, we are loading every team into -memory just to get the number of teams. We don't actually need any data from the team. +_many_ queries, leading to inefficient code. In this case, we are first executing a query to +get all sports, and then executing a query for each sport to get the number of teams in each +sport. And on top of that, we are loading every team into memory just to compute the number of +teams. We don't actually need any data from the team, only their aggregate count. -GRDB does not provide these kinds of tools, and for good reason. Instead, if you know you want to -fetch all of the teams with their corresponding sport, you can simply perform a single query that -joins the two tables together: +SharingGRDB does not provide these kinds of tools, and for good reason. Instead, if you know you +want to fetch all of the teams with their corresponding sport, you can simply perform a single +query that joins the two tables together: ```swift @Selection @@ -417,7 +468,7 @@ struct SportWithTeamCount { .fetchAll( Sport .group(by: \.id) - .join(Team.all) { $0.id.eq($1.sportID) } + .leftJoin(Team.all) { $0.id.eq($1.sportID) } .select { SportWithTeamCount.Columns(sport: $0, teamCount: $1.count()) } @@ -429,6 +480,10 @@ var sportsWithTeamCounts If either of the "sports" or "teams" tables change, this query will be executed again and the state will update to the freshest values. +This style of handling associations does require you to be knowledgable in SQL to yield it +correctly, but that is a benefit! SQL (and SQLite) are some of the most proven pieces of +technologies in the history of computers, and knowing how to wield their powers is a huge benefit. + ### Migrations [grdb-migration-docs]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/migrations @@ -503,7 +558,8 @@ adding a `description` field to the `Item` type: migrator.registerMigration("Add 'description' column to 'items'") { db in try #sql( """ - ALTER TABLE "items" ADD COLUMN "description" TEXT + ALTER TABLE "items" + ADD COLUMN "description" TEXT """ ) .execute(db) @@ -574,14 +630,17 @@ structure of your data types. The overall steps to follow are as such: try Item .where { !$0.id.in( - Item.select { $0.id.min() }.group(by: \.title) + Item + .select { $0.id.min() } + .group(by: \.title) ) } .execute() // 2️⃣ Create unique index try #sql( """ - CREATE UNIQUE INDEX "items_title" ON "items" ("title") + CREATE UNIQUE INDEX + "items_title" ON "items" ("title") """ ) .execute(db) diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md index d1cbdd34..3e3de684 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md @@ -19,11 +19,26 @@ connection. ### Querying with StructuredQueries [StructuredQueries][structured-queries-gh] is a library for building type-safe queries that safely -and performantly decode into Swift data types. For example, if you simply want to fetch all records -from a table, you can do so by plugging the query directly into -[`fetchAll`](): +and performantly decode into Swift data types. To get access to these tools you must apply +the `@Table` macro to your data type that represents the table: -@Comment { TODO: Add '@Table' definition? } +```swift +@Table +struct Item { + let id: Int + var title = "" + @Column(as: Date.ISO8601Representation.self) + var createdAt: Date +} +``` + +> Note: The `@Column` macro determines how to store the date in SQLite, which does not have a native +> date data type. The `Date.ISO8601Representation` strategy stores dates as text formatted with the +> ISO-8601 standard. + +Then you can use the various query builder APIs on `Item` to fetch items from the database. For +example, to fetch all records from the table you can use +[`fetchAll`](): ```swift @SharedReader(.fetchAll(Item.all) @@ -33,7 +48,7 @@ var items And if you want to sort the results, you can do so with an ordering clause: ```swift -@SharedReader(.fetchAll(Item.order { $0.createdAt.desc() })) +@SharedReader(.fetchAll(Item.order(by: \.title)) var items ``` @@ -53,7 +68,11 @@ type system every explicit cast and coalesce, you can always embed SQL directly the `#sql` macro: ```swift -@SharedReader(.fetchAll(Item.where { #sql("\($0.createdAt) > date('now', '-7 days')") })) +@SharedReader( + .fetchAll( + Item.where { #sql("\($0.createdAt) > date('now', '-7 days')") } + ) +) var items ``` @@ -79,38 +98,12 @@ The choice is up to you for each query or query fragment. To learn more, see the [structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries [structured-queries-docs]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/ -### Querying with raw SQL - -SharingGRDB also comes with a more basic set of tools that work directly with GRDB. The primary reason you -may want to use these tools and not the StructuredQueries tools is that they do not require a macro to use, -and so do not incur the cost of compiling SwiftSyntax. - -There is a version of [`fetchAll`]() key that -takes a raw SQL string: - -```swift -@SharedReader(.fetchAll(sql: "SELECT * FROM items")) var items: [Item] -``` - -As well as a [`fetchOne`]() key: - -```swift -@SharedReader(.fetchOne(sql: "SELECT count(*) FROM items")) -var itemsCount = 0 -``` - -These APIs simply feed their data directly to GRDB's equivalent `Database` APIs, which means it is -up to you to safely bind arguments and avoid SQL injection. If you want to write SQL queries by -hand, consider using StructuredQueries' `#sql` macro, instead. - -[structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries - ### Querying with custom requests -It is also possible to fetch data for a `@SharedReader` from a database connection. This can be -useful if you want to perform several queries in a single database transaction: +It is also possible to execute multiple database queries to fetch data for your `@SharedReader`. +This can be useful for performing several queries in a single database transaction: -Each instance of `@SharedReader` in a feature executes each of their queries in a separate +Each instance of `@SharedReader` in a feature executes their queries in a separate transaction. So, if we wanted to query for all in-stock items, as well as the count of all items (in-stock plus out-of-stock) like so: @@ -167,3 +160,30 @@ items.itemsCount // 100 Typically the conformances to ``FetchKeyRequest`` can even be made private and nested inside whatever type they are used in, such as SwiftUI view, `@Observable` model, or UIKit view controller. The only time it needs to be made public is if it's shared amongst many features. + +### Querying with raw SQL + +SharingGRDB also comes with a more basic set of tools that work directly with GRDB. The primary +reason you may want to use these tools and not the StructuredQueries tools is that they do not +require a macro to use (such as `@Table` and `#sql`), and so do not incur the cost of compiling +SwiftSyntax. + +There is a version of [`fetchAll`]() +key that takes a raw SQL string: + +```swift +@SharedReader(.fetchAll(sql: "SELECT * FROM items")) var items: [Item] +``` + +As well as a [`fetchOne`]() key: + +```swift +@SharedReader(.fetchOne(sql: "SELECT count(*) FROM items")) +var itemsCount = 0 +``` + +These APIs simply feed their data directly to GRDB's equivalent `Database` APIs, which means it is +up to you to safely bind arguments and avoid SQL injection. If you want to write SQL queries by +hand, consider using StructuredQueries' `#sql` macro, instead. + +[structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries diff --git a/Sources/SharingGRDB/FetchKey.swift b/Sources/SharingGRDB/FetchKey.swift index 073e9d9d..63c4fc52 100644 --- a/Sources/SharingGRDB/FetchKey.swift +++ b/Sources/SharingGRDB/FetchKey.swift @@ -401,3 +401,18 @@ private struct FetchOne: FetchKeyRequest { public struct NotFound: Error { public init() {} } + + +import SwiftData +@available(iOS 17, *) +@Model +class Reminder { + var title = "" + var isCompleted = false + var notes = "" + init(title: String = "", isCompleted: Bool = false, notes: String = "") { + self.title = title + self.isCompleted = isCompleted + self.notes = notes + } +} From 6e9336c51f27f6d13039c9c9bb09863ef2bc350d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Apr 2025 17:14:51 -0700 Subject: [PATCH 108/171] wip --- Sources/SharingGRDB/FetchKey.swift | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Sources/SharingGRDB/FetchKey.swift b/Sources/SharingGRDB/FetchKey.swift index 63c4fc52..073e9d9d 100644 --- a/Sources/SharingGRDB/FetchKey.swift +++ b/Sources/SharingGRDB/FetchKey.swift @@ -401,18 +401,3 @@ private struct FetchOne: FetchKeyRequest { public struct NotFound: Error { public init() {} } - - -import SwiftData -@available(iOS 17, *) -@Model -class Reminder { - var title = "" - var isCompleted = false - var notes = "" - init(title: String = "", isCompleted: Bool = false, notes: String = "") { - self.title = title - self.isCompleted = isCompleted - self.notes = notes - } -} From 7dda98e0182bf7e786c394d54b5c7fc8dc8e5feb Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Apr 2025 17:17:28 -0700 Subject: [PATCH 109/171] wip --- .../Documentation.docc/Articles/ComparisonWithSwiftData.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md index 2e6cbe36..2a39608e 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -480,7 +480,7 @@ var sportsWithTeamCounts If either of the "sports" or "teams" tables change, this query will be executed again and the state will update to the freshest values. -This style of handling associations does require you to be knowledgable in SQL to yield it +This style of handling associations does require you to be knowledgable in SQL to wield it correctly, but that is a benefit! SQL (and SQLite) are some of the most proven pieces of technologies in the history of computers, and knowing how to wield their powers is a huge benefit. From d458f12a47268fcdf1d876100f40ee151b360c5c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 17 Apr 2025 21:45:23 -0700 Subject: [PATCH 110/171] wip --- Examples/Reminders/ReminderForm.swift | 3 +- Examples/Reminders/ReminderRow.swift | 45 +++++++++++++++++-- Examples/Reminders/RemindersListDetail.swift | 2 +- Examples/Reminders/TagsForm.swift | 1 + .../Articles/ComparisonWithSwiftData.md | 1 + 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 6db36308..af3a3140 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -173,7 +173,8 @@ struct ReminderFormView: View { withErrorReporting { try database.write { db in let reminderID = try Reminder.upsert(reminder).returning(\.id).fetchOne(db)! - try ReminderTag.where { $0.reminderID.eq(reminderID) } + try ReminderTag + .where { $0.reminderID.eq(reminderID) } .delete() .execute(db) try ReminderTag.insert( diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 30483915..bca02b55 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -11,14 +11,34 @@ struct ReminderRow: View { let tags: [String] @State var editReminder: Reminder? + @State var isCompleted: Bool @Dependency(\.defaultDatabase) private var database + init( + color: Color, + isPastDue: Bool, + notes: String, + reminder: Reminder, + remindersList: RemindersList, + tags: [String], + editReminder: Reminder? = nil + ) { + self.color = color + self.isPastDue = isPastDue + self.notes = notes + self.reminder = reminder + self.remindersList = remindersList + self.tags = tags + self.editReminder = editReminder + self.isCompleted = reminder.isCompleted + } + var body: some View { HStack { HStack(alignment: .top) { Button(action: completeButtonTapped) { - Image(systemName: reminder.isCompleted ? "circle.inset.filled" : "circle") + Image(systemName: isCompleted ? "circle.inset.filled" : "circle") .foregroundStyle(.gray) .font(.title2) .padding([.trailing], 5) @@ -35,7 +55,7 @@ struct ReminderRow: View { } } Spacer() - if !reminder.isCompleted { + if !isCompleted { HStack { if reminder.isFlagged { Image(systemName: "flag.fill") @@ -79,9 +99,26 @@ struct ReminderRow: View { ReminderFormView(existingReminder: reminder, remindersList: remindersList) } } + .task(id: isCompleted) { + guard + isCompleted, + isCompleted != reminder.isCompleted + else { return } + do { + try await Task.sleep(for: .seconds(2)) + toggleCompletion() + } catch {} + } } private func completeButtonTapped() { + self.isCompleted.toggle() + if !self.isCompleted { + toggleCompletion() + } + } + + private func toggleCompletion() { withErrorReporting { try database.write { db in try Reminder @@ -119,9 +156,9 @@ struct ReminderRow: View { + (reminder.priority == nil ? "" : " ") return (Text(exclamations) - .foregroundStyle(reminder.isCompleted ? .gray : remindersList.color) + .foregroundStyle(isCompleted ? .gray : remindersList.color) + Text(reminder.title) - .foregroundStyle(reminder.isCompleted ? .gray : .primary)) + .foregroundStyle(isCompleted ? .gray : .primary)) .font(.title3) } } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 91026c58..eb5cbb08 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -235,7 +235,7 @@ struct RemindersListDetailView: View { reminder: $0, remindersList: $3, isPastDue: $0.isPastDue, - notes: $0.inlineNotes, + notes: $0.inlineNotes.substr(0, 200), tags: $2.jsonNames ) }, diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index bd85bec4..c8dbf3a2 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -59,6 +59,7 @@ struct TagsView: View { let rest = try Tag .where { !$0.id.in(top.map(\.id)) } + .order(by: \.title) .fetchAll(db) return Value(rest: rest, top: top) diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md index 2a39608e..a4a94a25 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -10,6 +10,7 @@ SwiftUI views (including UIKit, `@Observable` models, _etc._). This article desc approaches compare in a variety of situations, such as setting up the data store, fetching data, associations, and more. + * [Designing your schema](#Designing-your-schema) * [Setting up external storage](#Setting-up-external-storage) * [Fetching data for a view](#Fetching-data-for-a-view) * [Fetching data for an @Observable model](#Fetching-data-for-an-Observable-model) From ad5f5900c99afcb22601c330ce33d04e8bb4f693 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 17 Apr 2025 21:48:11 -0700 Subject: [PATCH 111/171] wip --- Examples/SyncUps/Schema.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 2f2271e6..22728c88 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -3,7 +3,7 @@ import StructuredQueriesGRDB import SwiftUI @Table -struct SyncUp: Codable, Hashable, Identifiable { +struct SyncUp: Hashable, Identifiable { let id: Int var seconds: Int = 60 * 5 var theme: Theme = .bubblegum @@ -11,14 +11,14 @@ struct SyncUp: Codable, Hashable, Identifiable { } @Table -struct Attendee: Codable, Hashable, Identifiable { +struct Attendee: Hashable, Identifiable { let id: Int var name = "" var syncUpID: SyncUp.ID } @Table -struct Meeting: Codable, Hashable, Identifiable { +struct Meeting: Hashable, Identifiable { let id: Int @Column(as: Date.ISO8601Representation.self) var date: Date @@ -26,7 +26,7 @@ struct Meeting: Codable, Hashable, Identifiable { var transcript: String } -enum Theme: String, CaseIterable, Codable, Hashable, Identifiable, QueryBindable { +enum Theme: String, CaseIterable, Hashable, Identifiable, QueryBindable { case appIndigo case appMagenta case appOrange From 86019d6839395bf97fd820bdfaffb5dab532a93a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Apr 2025 23:16:20 -0700 Subject: [PATCH 112/171] wip --- Examples/CaseStudies/DynamicQuery.swift | 4 +- Examples/Reminders/RemindersListDetail.swift | 2 +- Examples/Reminders/RemindersListForm.swift | 15 ++- Examples/Reminders/RemindersListRow.swift | 6 +- Examples/Reminders/RemindersLists.swift | 102 ++++++++++++++----- 5 files changed, 96 insertions(+), 33 deletions(-) diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index 20289b56..9d3b0b18 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -39,10 +39,10 @@ struct DynamicQueryDemo: SwiftUICaseStudy { ForEach(facts.facts) { fact in Text(fact.body) } - .onDelete { indexSet in + .onDelete { indices in withErrorReporting { try database.write { db in - let ids = indexSet.map { facts.facts[$0].id } + let ids = indices.map { facts.facts[$0].id } try Fact .where { $0.id.in(ids) } .delete() diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index eb5cbb08..3586edc5 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -84,7 +84,7 @@ struct RemindersListDetailView: View { } label: { HStack { Image(systemName: "plus.circle.fill") - Text("New reminder") + Text("New Reminder") } .bold() .font(.title3) diff --git a/Examples/Reminders/RemindersListForm.swift b/Examples/Reminders/RemindersListForm.swift index 69e69338..aadab767 100644 --- a/Examples/Reminders/RemindersListForm.swift +++ b/Examples/Reminders/RemindersListForm.swift @@ -15,9 +15,21 @@ struct RemindersListForm: View { var body: some View { Form { - TextField("Name", text: $remindersList.title) + Section { + VStack { + TextField("List Name", text: $remindersList.title) + .font(.system(.title2, design: .rounded, weight: .bold)) + .foregroundStyle(remindersList.color) + .multilineTextAlignment(.center) + .padding() + .textFieldStyle(.plain) + } + .background(Color(.secondarySystemBackground)) + .clipShape(.buttonBorder) + } ColorPicker("Color", selection: $remindersList.color) } + .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem { Button("Save") { @@ -45,5 +57,6 @@ struct RemindersListForm: View { } NavigationStack { RemindersListForm() + .navigationTitle("New List") } } diff --git a/Examples/Reminders/RemindersListRow.swift b/Examples/Reminders/RemindersListRow.swift index 4ecb3b9b..b29279b3 100644 --- a/Examples/Reminders/RemindersListRow.swift +++ b/Examples/Reminders/RemindersListRow.swift @@ -13,11 +13,15 @@ struct RemindersListRow: View { var body: some View { HStack { Image(systemName: "list.bullet.circle.fill") - .font(.title) + .font(.largeTitle) .foregroundStyle(remindersList.color) + .background( + Color.white.clipShape(Circle()).padding(4) + ) Text(remindersList.title) Spacer() Text("\(reminderCount)") + .foregroundStyle(.gray) } .swipeActions { Button { diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 4ee40ae4..4f0b6cfc 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -16,7 +16,6 @@ struct RemindersListsView: View { @Selection fileprivate struct Stats { var allCount = 0 - var completedCount = 0 var flaggedCount = 0 var scheduledCount = 0 var todayCount = 0 @@ -43,7 +42,6 @@ struct RemindersListsView: View { Reminder.select { Stats.Columns( allCount: $0.count(filter: !$0.isCompleted), - completedCount: $0.count(filter: $0.isCompleted), flaggedCount: $0.count(filter: $0.isFlagged), scheduledCount: $0.count(filter: $0.isScheduled), todayCount: $0.count(filter: $0.isToday) @@ -53,9 +51,16 @@ struct RemindersListsView: View { ) private var stats = Stats() - @State private var isAddListPresented = false + enum Destination: Int, Identifiable { + case addList + case newReminder + + var id: Int { rawValue } + } + + @State private var destination: Destination? + @State private var remindersDetailType: RemindersListDetailView.DetailType? @State private var searchText = "" - @State var remindersDetailType: RemindersListDetailView.DetailType? @Dependency(\.defaultDatabase) private var database @@ -63,7 +68,7 @@ struct RemindersListsView: View { List { if searchText.isEmpty { Section { - Grid(horizontalSpacing: 16, verticalSpacing: 16) { + Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 16) { GridRow { ReminderGridCell( color: .blue, @@ -103,7 +108,7 @@ struct RemindersListsView: View { GridRow { ReminderGridCell( color: .gray, - count: stats.completedCount, + count: nil, iconName: "checkmark.circle.fill", title: "Completed" ) { @@ -111,8 +116,10 @@ struct RemindersListsView: View { } } } + .buttonStyle(.plain) + .listRowBackground(Color.clear) + .padding([.leading, .trailing], -20) } - .buttonStyle(.plain) Section { ForEach(remindersLists) { state in @@ -126,29 +133,59 @@ struct RemindersListsView: View { } } } header: { - Text("My lists") - .font(.largeTitle) - .bold() - .foregroundStyle(.black) + Text("My Lists") + .font(.system(.title2, design: .rounded, weight: .bold)) + .foregroundStyle(Color(.label)) + .textCase(nil) + .padding(.top, -16) + .padding([.leading, .trailing], 4) } + .listRowInsets(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)) } else { SearchRemindersView(searchText: searchText) } } // NB: This explicit view identity works around a bug with 'List' view state not getting reset. .id(searchText) - .listStyle(.plain) + .listStyle(.insetGrouped) .toolbar { - Button("Add list") { - isAddListPresented = true + ToolbarItem(placement: .bottomBar) { + HStack { + Button { + destination = .newReminder + } label: { + HStack { + Image(systemName: "plus.circle.fill") + Text("New Reminder") + } + .bold() + .font(.title3) + } + Spacer() + Button { + destination = .addList + } label: { + Text("Add List") + .font(.title3) + } + } } } - .sheet(isPresented: $isAddListPresented) { - NavigationStack { - RemindersListForm() - .navigationTitle("New list") + .sheet(item: $destination) { destination in + switch destination { + case .addList: + NavigationStack { + RemindersListForm() + .navigationTitle("New List") + } + .presentationDetents([.medium]) + case .newReminder: + if let remindersList = remindersLists.first?.remindersList { + NavigationStack { + ReminderFormView(remindersList: remindersList) + } + } } - .presentationDetents([.medium]) } .searchable(text: $searchText) .navigationDestination(item: $remindersDetailType) { detailType in @@ -159,30 +196,39 @@ struct RemindersListsView: View { private struct ReminderGridCell: View { let color: Color - let count: Int + let count: Int? let iconName: String let title: String let action: () -> Void var body: some View { Button(action: action) { - HStack(alignment: .top) { - VStack(alignment: .leading) { + HStack(alignment: .firstTextBaseline) { + VStack(alignment: .leading, spacing: 8) { Image(systemName: iconName) .font(.largeTitle) .bold() .foregroundStyle(color) + .background( + Color.white.clipShape(Circle()).padding(4) + ) Text(title) + .font(.headline) + .foregroundStyle(.gray) .bold() + .padding(.leading, 4) } Spacer() - Text("\(count)") - .font(.largeTitle) - .fontDesign(.rounded) - .bold() + if let count { + Text("\(count)") + .font(.largeTitle) + .fontDesign(.rounded) + .bold() + .foregroundStyle(Color(.label)) + } } - .padding() - .background(.black.opacity(0.05)) + .padding(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)) + .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(10) } } From 963c15b59f9f283ac6f1b72d163fdd01c05c4797 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Apr 2025 23:29:31 -0700 Subject: [PATCH 113/171] wip --- Examples/Reminders/ReminderForm.swift | 2 +- Examples/Reminders/RemindersListDetail.swift | 2 +- Examples/Reminders/TagsForm.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index af3a3140..eeeb4e4b 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -50,7 +50,7 @@ struct ReminderFormView: View { .font(.title) .foregroundStyle(.gray) Text("Tags") - .foregroundStyle(.black) + .foregroundStyle(Color(.label)) Spacer() if let tagsDetail { tagsDetail diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 3586edc5..5d9579fa 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -20,7 +20,7 @@ struct RemindersListDetailView: View { self.detailType = detailType _ordering = AppStorage(wrappedValue: .dueDate, "ordering_list_\(detailType.tag)") _showCompleted = AppStorage( - wrappedValue: detailType != .completed, + wrappedValue: detailType == .completed, "show_completed_list_\(detailType.tag)" ) _reminderStates = SharedReader(wrappedValue: [], remindersKey) diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index c8dbf3a2..ca1ad6db 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -91,7 +91,7 @@ private struct TagView: View { Text(tag.title) } } - .tint(isSelected ? .blue : .black) + .tint(isSelected ? .accentColor : .primary) } } From f8d04443462b51b69daa239046f3e829b2799363 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Apr 2025 23:50:06 -0700 Subject: [PATCH 114/171] wip --- Examples/Reminders/ReminderRow.swift | 15 +++++++++++---- Examples/Reminders/RemindersListDetail.swift | 1 + Examples/Reminders/SearchReminders.swift | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index bca02b55..6d5ea4ed 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -8,6 +8,7 @@ struct ReminderRow: View { let notes: String let reminder: Reminder let remindersList: RemindersList + let showCompleted: Bool let tags: [String] @State var editReminder: Reminder? @@ -21,6 +22,7 @@ struct ReminderRow: View { notes: String, reminder: Reminder, remindersList: RemindersList, + showCompleted: Bool, tags: [String], editReminder: Reminder? = nil ) { @@ -29,6 +31,7 @@ struct ReminderRow: View { self.notes = notes self.reminder = reminder self.remindersList = remindersList + self.showCompleted = showCompleted self.tags = tags self.editReminder = editReminder self.isCompleted = reminder.isCompleted @@ -100,6 +103,7 @@ struct ReminderRow: View { } } .task(id: isCompleted) { + guard !showCompleted else { return } guard isCompleted, isCompleted != reminder.isCompleted @@ -112,19 +116,21 @@ struct ReminderRow: View { } private func completeButtonTapped() { - self.isCompleted.toggle() - if !self.isCompleted { + if showCompleted { toggleCompletion() + } else { + isCompleted.toggle() } } private func toggleCompletion() { withErrorReporting { try database.write { db in - try Reminder + isCompleted = try Reminder .find(reminder.id) .update { $0.isCompleted.toggle() } - .execute(db) + .returning(\.isCompleted) + .fetchOne(db) ?? isCompleted } } } @@ -183,6 +189,7 @@ struct ReminderRowPreview: PreviewProvider { notes: reminder.notes.replacingOccurrences(of: "\n", with: " "), reminder: reminder, remindersList: remindersList, + showCompleted: true, tags: ["point-free", "adulting"] ) } diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 5d9579fa..aed60e8d 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -44,6 +44,7 @@ struct RemindersListDetailView: View { notes: reminderState.notes, reminder: reminderState.reminder, remindersList: reminderState.remindersList, + showCompleted: showCompleted, tags: reminderState.tags ) } diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 7a7f865e..ac013833 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -52,6 +52,7 @@ struct SearchRemindersView: View { notes: reminder.notes, reminder: reminder.reminder, remindersList: reminder.remindersList, + showCompleted: showCompletedInSearchResults, tags: reminder.tags ) } From a65a9bb1bdc953f112fce51d29eb797ae2f6ea2d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 00:13:36 -0700 Subject: [PATCH 115/171] wip --- Examples/Reminders/ReminderForm.swift | 4 +++- Examples/Reminders/ReminderRow.swift | 1 + Examples/Reminders/RemindersListDetail.swift | 1 + Examples/Reminders/RemindersLists.swift | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index eeeb4e4b..8d9e42d8 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -131,6 +131,7 @@ struct ReminderFormView: View { } } } + .padding(.top, -28) .task { guard let reminderID = reminder.id else { return } @@ -147,7 +148,7 @@ struct ReminderFormView: View { reportIssue(error) } } - .navigationTitle(remindersList.title) + .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem { Button(action: saveButtonTapped) { @@ -217,6 +218,7 @@ struct ReminderFormPreview: PreviewProvider { } NavigationStack { ReminderFormView(existingReminder: reminder, remindersList: remindersList) + .navigationTitle("Detail") } } } diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 6d5ea4ed..f19a0724 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -100,6 +100,7 @@ struct ReminderRow: View { .sheet(item: $editReminder) { reminder in NavigationStack { ReminderFormView(existingReminder: reminder, remindersList: remindersList) + .navigationTitle("Details") } } .task(id: isCompleted) { diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index aed60e8d..8df2e4d7 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -59,6 +59,7 @@ struct RemindersListDetailView: View { if let remindersList = detailType.list { NavigationStack { ReminderFormView(remindersList: remindersList) + .navigationTitle("New Reminder") } } } diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 4f0b6cfc..14eb60ce 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -183,6 +183,7 @@ struct RemindersListsView: View { if let remindersList = remindersLists.first?.remindersList { NavigationStack { ReminderFormView(remindersList: remindersList) + .navigationTitle("New Reminder") } } } From 0d460486c4399071abcb1e3049edccfc7206f666 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 18 Apr 2025 08:24:56 -0700 Subject: [PATCH 116/171] clean up --- Examples/Reminders/RemindersListDetail.swift | 85 ++++++++------------ 1 file changed, 35 insertions(+), 50 deletions(-) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 8df2e4d7..202b101f 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -18,10 +18,10 @@ struct RemindersListDetailView: View { init(detailType: DetailType) { self.detailType = detailType - _ordering = AppStorage(wrappedValue: .dueDate, "ordering_list_\(detailType.tag)") + _ordering = AppStorage(wrappedValue: .dueDate, "ordering_list_\(detailType.id)") _showCompleted = AppStorage( wrappedValue: detailType == .completed, - "show_completed_list_\(detailType.tag)" + "show_completed_list_\(detailType.id)" ) _reminderStates = SharedReader(wrappedValue: [], remindersKey) } @@ -150,54 +150,6 @@ struct RemindersListDetailView: View { case list(RemindersList) case scheduled case today - var tag: String { - switch self { - case .all: - "all" - case .completed: - "completed" - case .flagged: - "flagged" - case .list(let list): - "list_\(list.id)" - case .scheduled: - "scheduled" - case .today: - "today" - } - } - var navigationTitle: String { - switch self { - case .all: - "All" - case .completed: - "Completed" - case .flagged: - "Flagged" - case .list(let list): - list.title - case .scheduled: - "Scheduled" - case .today: - "Today" - } - } - var color: Color { - switch self { - case .all: - .black - case .completed: - .gray - case .flagged: - .orange - case .list(let list): - list.color - case .scheduled: - .red - case .today: - .blue - } - } } private func updateQuery() async throws { @@ -257,6 +209,39 @@ struct RemindersListDetailView: View { } } +extension RemindersListDetailView.DetailType { + fileprivate var id: String { + switch self { + case .all: "all" + case .completed: "completed" + case .flagged: "flagged" + case .list(let list): "list_\(list.id)" + case .scheduled: "scheduled" + case .today: "today" + } + } + fileprivate var navigationTitle: String { + switch self { + case .all: "All" + case .completed: "Completed" + case .flagged: "Flagged" + case .list(let list): list.title + case .scheduled: "Scheduled" + case .today: "Today" + } + } + fileprivate var color: Color { + switch self { + case .all: .black + case .completed: .gray + case .flagged: .orange + case .list(let list): list.color + case .scheduled: .red + case .today: .blue + } + } +} + struct RemindersListDetailPreview: PreviewProvider { static var previews: some View { let remindersList = try! prepareDependencies { From 9ed19f8529f0b9d45878748dd07c9c810320faee Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 10:49:26 -0700 Subject: [PATCH 117/171] wip --- Examples/Reminders/ReminderRow.swift | 23 +++---- README.md | 66 +++++++++++++++++-- .../Articles/ComparisonWithSwiftData.md | 8 +-- .../Documentation.docc/SharingGRDB.md | 64 +++++++++++++++++- 4 files changed, 137 insertions(+), 24 deletions(-) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index f19a0724..f118361e 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -39,7 +39,7 @@ struct ReminderRow: View { var body: some View { HStack { - HStack(alignment: .top) { + HStack(alignment: .firstTextBaseline) { Button(action: completeButtonTapped) { Image(systemName: isCompleted ? "circle.inset.filled" : "circle") .foregroundStyle(.gray) @@ -51,8 +51,9 @@ struct ReminderRow: View { if !notes.isEmpty { Text(notes) - .lineLimit(2) + .font(.subheadline) .foregroundStyle(.gray) + .lineLimit(2) } subtitleText } @@ -158,15 +159,15 @@ struct ReminderRow: View { } private func title(for reminder: Reminder) -> some View { - let exclamations = - String(repeating: "!", count: reminder.priority?.rawValue ?? 0) - + (reminder.priority == nil ? "" : " ") - return - (Text(exclamations) - .foregroundStyle(isCompleted ? .gray : remindersList.color) - + Text(reminder.title) - .foregroundStyle(isCompleted ? .gray : .primary)) - .font(.title3) + return HStack(alignment: .firstTextBaseline) { + if let priority = reminder.priority { + Text(String(repeating: "!", count: priority.rawValue)) + .foregroundStyle(isCompleted ? .gray : remindersList.color) + } + Text(reminder.title) + .foregroundStyle(isCompleted ? .gray : .primary) + } + .font(.title3) } } diff --git a/README.md b/README.md index 5f276a44..234dfc36 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SharingGRDB -A lightweight replacement for SwiftData and `@Query`. +A fast, lightweight replacement for SwiftData, powered by SQL. [![CI](https://github.com/pointfreeco/sharing-grdb/workflows/CI/badge.svg)](https://github.com/pointfreeco/sharing-grdb/actions?query=workflow%3ACI) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fsharing-grdb%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pointfreeco/sharing-grdb) @@ -28,8 +28,8 @@ library, [subscribe today](https://www.pointfree.co/pricing). ## Overview -SharingGRDB is lightweight replacement for SwiftData and the `@Query` macro that deploys all the way -back to the iOS 13 generation of targets. +SharingGRDB is a [fast](#performance), lightweight replacement for SwiftData that deploys all the +way back to the iOS 13 generation of targets. @@ -42,6 +42,14 @@ back to the iOS 13 generation of targets. ```swift @SharedReader(.fetchAll(Item.order(by: \.title))) var items + +@Table +struct Item { + let id: Int + var title = "" + var isInStock = true + var notes = "" +} ``` @@ -50,6 +58,22 @@ var items ```swift @Query(sort: \Item.title) var items: [Item] + +@Model +class Item { + var title: String + var isInStock: Bool + var notes: String + init( + title: String = "", + isInStock: Bool = true, + notes: String = "" + ) { + self.title = title + self.isInStock = isInStock + self.notes = notes + } +} ``` @@ -61,7 +85,13 @@ observed by SwiftUI so that views are recomputed when the external data changes, powered directly by SQLite using [Sharing][sharing-gh] and [GRDB][grdb], and is usable from UIKit, `@Observable` models, and more. -For more information on SharingGRDB's querying capabilities, see [Fetching model data][fetching-article]. +> [!TIP] +> The `@Table` macro comes from +> [StructuredQueries](https://github.com/pointfreeco/swift-structured-queries), a general purpose +> library for building and decoding SQL. + +For more information on SharingGRDB's querying capabilities, see +[Fetching model data][fetching-article]. ## Quick start @@ -118,7 +148,8 @@ struct MyApp: App {
-> Note: For more information on preparing a SQLite database, see +> [!NOTE] +> For more information on preparing a SQLite database, see > [Preparing a SQLite database][preparing-db-article]. This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies, like @@ -167,7 +198,8 @@ try modelContext.save() -> Note: For more information on how SharingGRDB compares to SwiftData, see +> [!NOTE] +> For more information on how SharingGRDB compares to SwiftData, see > [Comparison with SwiftData][comparison-swiftdata-article]. This is all you need to know to get started with SharingGRDB, but there's much more to learn. Read @@ -187,6 +219,28 @@ the [articles][articles] below to learn how to best utilize this library: [preparing-db-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/preparingdatabase [fetchall-docs]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/sharing/sharedreaderkey/fetchall(sql:arguments:database:animation:) +## Performance + +SharingGRDB leverages high-performance decoding from +[StructuredQueries][StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to +turn fetched data into your Swift domain types, and has a performance profile similar to invoking +SQLite's C APIs directly. + +See the following benchmarks from +[Lighter's performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite) for a +taste of how it compares: + +``` +Orders.fetchAll setup rampup duration + SQLite (Enlighter-generated) 0 0.144 7.183 + Lighter (1.4.10) 0 0.164 8.059 + SharingGRDB (0.2.0) 0 0.172 8.511 + GRDB (7.4.0, manual decoding) 0 0.376 18.819 + SQLite.swift (0.15.3, manual decoding) 0 0.564 27.994 + SQLite.swift (0.15.3, Codable) 0 0.863 43.261 + GRDB (7.4.0, Codable) 0.002 1.07 53.326 +``` + ## SQLite knowledge required SQLite is one of the diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md index a4a94a25..c2e068dd 100644 --- a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -68,10 +68,10 @@ to SwiftData's `@Model` macro: Some key differences: -* The `@Table` macro works with struct data types, whereas `@Model` only works with classes. -* Because the `@Model` version of `Item` is a class it is necessary to provide an initializer. -* The `@Model` version of `Item` does not need an `id` field because SwiftData provides a -`persistentIdentifier` to each model. + * The `@Table` macro works with struct data types, whereas `@Model` only works with classes. + * Because the `@Model` version of `Item` is a class it is necessary to provide an initializer. + * The `@Model` version of `Item` does not need an `id` field because SwiftData provides a + `persistentIdentifier` to each model. ### Setting up external storage diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index d4226cee..2849a661 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -11,6 +11,14 @@ back to the iOS 13 generation of targets. // SharingGRDB @SharedReader(.fetchAll(Item.all)) var items + + @Table + struct Item { + let id: Int + var title = "" + var isInStock = true + var notes = "" + } ``` } @Column { @@ -18,14 +26,31 @@ back to the iOS 13 generation of targets. // SwiftData @Query var items: [Item] + + @Model + class Item { + var title: String + var isInStock: Bool + var notes: String + init( + title: String = "", + isInStock: Bool = true, + notes: String = "" + ) { + self.title = title + self.isInStock = isInStock + self.notes = notes + } + } ``` } } Both of the above examples fetch items from an external data store, and both are automatically observed by SwiftUI so that views are recomputed when the external data changes, but SharingGRDB is -powered directly by SQLite using [Sharing](#What-is-Sharing) and [GRDB](#What-is-GRDB), and is -usable from UIKit, `@Observable` models, and more. +powered directly by SQLite using [Sharing](#What-is-Sharing), +[StructuredQueries](#What-is-StructuredQueries), and [GRDB](#What-is-GRDB), and is usable from +anywhere, including UIKit, `@Observable` models, and more. > Note: For more information on SharingGRDB's querying capabilities, see . @@ -117,6 +142,28 @@ a model context, via a property wrapper: This is all you need to know to get started with SharingGRDB, but there's much more to learn. Read the [articles](#Essentials) below to learn how to best utilize this library. +## Performance + +SharingGRDB leverages high-performance decoding from +[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to turn fetched data +into your Swift domain types, and has a performance profile similar to invoking SQLite's C APIs +directly. + +See the following benchmarks from +[Lighter's performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite) for a +taste of how it compares: + +``` +Orders.fetchAll setup rampup duration + SQLite (Enlighter-generated) 0 0.144 7.183 + Lighter (1.4.10) 0 0.164 8.059 + SharingGRDB (0.2.0) 0 0.172 8.511 + GRDB (7.4.0, manual decoding) 0 0.376 18.819 + SQLite.swift (0.15.3, manual decoding) 0 0.564 27.994 + SQLite.swift (0.15.3, Codable) 0 0.863 43.261 + GRDB (7.4.0, Codable) 0.002 1.07 53.326 +``` + ## SQLite knowledge required SQLite is one of the @@ -144,7 +191,18 @@ This is all you need to know about Sharing to hit the ground running with Sharin scratches the surface of what the library makes possible. It can also act as a replacement to SwiftUI's `@AppStorage` that works with UIKit and `@Observable` models, and can be integrated with custom persistence strategies. To learn more, check out -[the documentation](https://swiftpackageindex.com/pointfreeco/swift-sharing/main/documentation/sharing/). +[the documentation](https://swiftpackageindex.com/pointfreeco/swift-sharing/~/documentation/sharing/). + +## What is StructuredQueries? + +[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) is a library for +building SQL in a safe, expressive, and composable manner, and decoding results with high +performance. Learn more about designing schemas and building queries with the library by seeing its +[documentation](https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/). + +SharingGRDB contains an official StructuredQueries driver that connects it to SQLite _via_ GRDB, +though its query builder and decoder are general purpose tools that can interface with other +databases (MySQL, Postgres, _etc._) and database libraries. ## What is GRDB? From d9cfaa603e0d1535e89b812bee8784b31b660030 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 10:50:34 -0700 Subject: [PATCH 118/171] wip --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 234dfc36..ab5b50aa 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ way back to the iOS 13 generation of targets. ```swift -@SharedReader(.fetchAll(Item.order(by: \.title))) +@SharedReader(.fetchAll(Item.all)) var items @Table @@ -56,7 +56,7 @@ struct Item { ```swift -@Query(sort: \Item.title) +@Query var items: [Item] @Model From 0b7b0fa0c18a58fe6973783ea464016440f6fc7a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 10:53:12 -0700 Subject: [PATCH 119/171] wip --- README.md | 12 +++++++----- .../SharingGRDB/Documentation.docc/SharingGRDB.md | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ab5b50aa..4655df48 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,11 @@ class Item { -Both of the above examples fetch items from an external data store, and both are automatically -observed by SwiftUI so that views are recomputed when the external data changes, but SharingGRDB is -powered directly by SQLite using [Sharing][sharing-gh] and [GRDB][grdb], and is -usable from UIKit, `@Observable` models, and more. +Both of the above examples fetch items from an external data store using Swift data types, and both +are automatically observed by SwiftUI so that views are recomputed when the external data changes, +but SharingGRDB is powered directly by SQLite using [Sharing][sharing-gh], +[StructuredQueries][structured-queries-gh], and [GRDB][grdb], and is usable from UIKit, +`@Observable` models, and more. > [!TIP] > The `@Table` macro comes from @@ -257,7 +258,8 @@ along with all of the power that SQL has to offer. [query-interface]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/queryinterface [sharing-gh]: http://github.com/pointfreeco/swift-sharing -[grdb]: http://github.com/groue/grdb.swift +[structured-queries-gh] http://github.com/pointfreeco/swift-structured-queries +[grdb]: http://github.com/groue/GRDB.swift [swift-nav-gh]: https://github.com/pointfreeco/swift-navigation [observe-docs]: https://swiftpackageindex.com/pointfreeco/swift-navigation/main/documentation/swiftnavigation/objectivec/nsobject/observe(_:)-94oxy diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index 2849a661..a39162a4 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -46,9 +46,9 @@ back to the iOS 13 generation of targets. } } -Both of the above examples fetch items from an external data store, and both are automatically -observed by SwiftUI so that views are recomputed when the external data changes, but SharingGRDB is -powered directly by SQLite using [Sharing](#What-is-Sharing), +Both of the above examples fetch items from an external data store using Swift data types, and both +are automatically observed by SwiftUI so that views are recomputed when the external data changes, +but SharingGRDB is powered directly by SQLite using [Sharing](#What-is-Sharing), [StructuredQueries](#What-is-StructuredQueries), and [GRDB](#What-is-GRDB), and is usable from anywhere, including UIKit, `@Observable` models, and more. From e7529947f2b30640cffa2f090d2bbe8f24c7a467 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 10:55:02 -0700 Subject: [PATCH 120/171] wip --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4655df48..9b087814 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,8 @@ but SharingGRDB is powered directly by SQLite using [Sharing][sharing-gh], `@Observable` models, and more. > [!TIP] -> The `@Table` macro comes from -> [StructuredQueries](https://github.com/pointfreeco/swift-structured-queries), a general purpose -> library for building and decoding SQL. +> `@SharedReader` comes from the [Sharing][sharing-gh] library, while the `@Table` macro comes from +> [StructuredQueries][structured-queries-gh]. For more information on SharingGRDB's querying capabilities, see [Fetching model data][fetching-article]. From 69c78f6d0486f24741c55f76ec412c2c77003ba2 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 10:55:43 -0700 Subject: [PATCH 121/171] wip --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9b087814..5251523a 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,10 @@ but SharingGRDB is powered directly by SQLite using [Sharing][sharing-gh], For more information on SharingGRDB's querying capabilities, see [Fetching model data][fetching-article]. +[sharing-gh]: http://github.com/pointfreeco/swift-sharing +[structured-queries-gh] http://github.com/pointfreeco/swift-structured-queries +[grdb]: http://github.com/groue/GRDB.swift + ## Quick start Before SharingGRDB's property wrappers can fetch data from SQLite, you need to provide–at From 2910fca42477af62ef10beafef1f425be44c05fa Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 10:56:28 -0700 Subject: [PATCH 122/171] wip --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5251523a..fe92bce2 100644 --- a/README.md +++ b/README.md @@ -93,10 +93,6 @@ but SharingGRDB is powered directly by SQLite using [Sharing][sharing-gh], For more information on SharingGRDB's querying capabilities, see [Fetching model data][fetching-article]. -[sharing-gh]: http://github.com/pointfreeco/swift-sharing -[structured-queries-gh] http://github.com/pointfreeco/swift-structured-queries -[grdb]: http://github.com/groue/GRDB.swift - ## Quick start Before SharingGRDB's property wrappers can fetch data from SQLite, you need to provide–at @@ -260,9 +256,9 @@ for data and keep your views up-to-date when data in the database changes. You c along with all of the power that SQL has to offer. [query-interface]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/queryinterface -[sharing-gh]: http://github.com/pointfreeco/swift-sharing -[structured-queries-gh] http://github.com/pointfreeco/swift-structured-queries -[grdb]: http://github.com/groue/GRDB.swift +[sharing-gh]: https://github.com/pointfreeco/swift-sharing +[structured-queries-gh] https://github.com/pointfreeco/swift-structured-queries +[grdb]: https://github.com/groue/GRDB.swift [swift-nav-gh]: https://github.com/pointfreeco/swift-navigation [observe-docs]: https://swiftpackageindex.com/pointfreeco/swift-navigation/main/documentation/swiftnavigation/objectivec/nsobject/observe(_:)-94oxy From 07b2a819de2b730cb250fcb2254d05a8c07c7c9c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 10:57:38 -0700 Subject: [PATCH 123/171] wip --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fe92bce2..5cb8112e 100644 --- a/README.md +++ b/README.md @@ -82,13 +82,11 @@ class Item { Both of the above examples fetch items from an external data store using Swift data types, and both are automatically observed by SwiftUI so that views are recomputed when the external data changes, -but SharingGRDB is powered directly by SQLite using [Sharing][sharing-gh], -[StructuredQueries][structured-queries-gh], and [GRDB][grdb], and is usable from UIKit, -`@Observable` models, and more. - -> [!TIP] -> `@SharedReader` comes from the [Sharing][sharing-gh] library, while the `@Table` macro comes from -> [StructuredQueries][structured-queries-gh]. +but SharingGRDB is powered directly by SQLite using +[Sharing](https://github.com/pointfreeco/swift-sharing), +[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries), and +[GRDB](https://github.com/groue/GRDB.swift), and is usable from UIKit, `@Observable` models, and +more. For more information on SharingGRDB's querying capabilities, see [Fetching model data][fetching-article]. From 95084b64b8f9a2804b2e72cabe32723af83b59b8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 10:58:17 -0700 Subject: [PATCH 124/171] wip --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5cb8112e..448ecc19 100644 --- a/README.md +++ b/README.md @@ -220,9 +220,9 @@ the [articles][articles] below to learn how to best utilize this library: ## Performance SharingGRDB leverages high-performance decoding from -[StructuredQueries][StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to -turn fetched data into your Swift domain types, and has a performance profile similar to invoking -SQLite's C APIs directly. +[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to turn fetched data +into your Swift domain types, and has a performance profile similar to invoking SQLite's C APIs +directly. See the following benchmarks from [Lighter's performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite) for a From a7ffa3172c0d4566f31072ce6cb4fbd262945bcf Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 10:59:44 -0700 Subject: [PATCH 125/171] wip --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 448ecc19..ac72022e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SharingGRDB -A fast, lightweight replacement for SwiftData, powered by SQL. +A [fast](#Performance), lightweight replacement for SwiftData, powered by SQL. [![CI](https://github.com/pointfreeco/sharing-grdb/workflows/CI/badge.svg)](https://github.com/pointfreeco/sharing-grdb/actions?query=workflow%3ACI) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fsharing-grdb%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pointfreeco/sharing-grdb) @@ -8,6 +8,10 @@ A fast, lightweight replacement for SwiftData, powered by SQL. * [Learn more](#Learn-more) * [Overview](#Overview) + * [Quick start](#Quick-start) + * [Performance](#Performance) + * [SQLite knowledge required](#SQLite-knowledge-required) + * [Overview](#Overview) * [Demos](#Demos) * [Documentation](#Documentation) * [Installation](#Installation) From 695b3d90ee524c9d2afae2a806f0d46cc334d578 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 11:06:18 -0700 Subject: [PATCH 126/171] wip --- README.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ac72022e..c4d9bde0 100644 --- a/README.md +++ b/README.md @@ -86,11 +86,9 @@ class Item { Both of the above examples fetch items from an external data store using Swift data types, and both are automatically observed by SwiftUI so that views are recomputed when the external data changes, -but SharingGRDB is powered directly by SQLite using -[Sharing](https://github.com/pointfreeco/swift-sharing), -[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries), and -[GRDB](https://github.com/groue/GRDB.swift), and is usable from UIKit, `@Observable` models, and -more. +but SharingGRDB is powered directly by SQLite using [Sharing][sharing-gh], +[StructuredQueries][structured-queries-gh], and [GRDB][grdb], and is usable from UIKit, +`@Observable` models, and more. For more information on SharingGRDB's querying capabilities, see [Fetching model data][fetching-article]. @@ -177,9 +175,9 @@ a model context, via a property wrapper: @Dependency(\.defaultDatabase) var database -var newItem = +let newItem = Item(/* ... */) try database.write { db in - try Item.insert(Item(/* ... */)) + try Item.insert(newItem) .execute(db)) } ``` @@ -223,10 +221,9 @@ the [articles][articles] below to learn how to best utilize this library: ## Performance -SharingGRDB leverages high-performance decoding from -[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to turn fetched data -into your Swift domain types, and has a performance profile similar to invoking SQLite's C APIs -directly. +SharingGRDB leverages high-performance decoding from [StructuredQueries][structured-queries-gh] to +turn fetched data into your Swift domain types, and has a performance profile similar to invoking +SQLite's C APIs directly. See the following benchmarks from [Lighter's performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite) for a @@ -259,7 +256,7 @@ along with all of the power that SQL has to offer. [query-interface]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/queryinterface [sharing-gh]: https://github.com/pointfreeco/swift-sharing -[structured-queries-gh] https://github.com/pointfreeco/swift-structured-queries +[structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries [grdb]: https://github.com/groue/GRDB.swift [swift-nav-gh]: https://github.com/pointfreeco/swift-navigation [observe-docs]: https://swiftpackageindex.com/pointfreeco/swift-navigation/main/documentation/swiftnavigation/objectivec/nsobject/observe(_:)-94oxy From 352623c25b1b2840f9fb1c89e18793c6860e152c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 11:09:12 -0700 Subject: [PATCH 127/171] wip --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c4d9bde0..8ba93338 100644 --- a/README.md +++ b/README.md @@ -250,9 +250,9 @@ library you should be familiar with the basics of SQLite, including schema desig SQL queries, including joins and aggregates, and performance, including indices. With some basic knowledge you can apply this library to your database schema in order to query -for data and keep your views up-to-date when data in the database changes. You can use GRDB's -[query builder][query-interface] APIs to query your database, or you can use raw SQL queries, -along with all of the power that SQL has to offer. +for data and keep your views up-to-date when data in the database changes, and you can use +[StructuredQueries][structured-queries-gh] to build queries, either using its type-safe, +discoverable query builder, or using its `#sql` macro for writing safe SQL strings. [query-interface]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/queryinterface [sharing-gh]: https://github.com/pointfreeco/swift-sharing From 1561d35a1d4f799d77e1d550530b5154f1b7c98a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 11:11:17 -0700 Subject: [PATCH 128/171] wip --- README.md | 4 ++-- Sources/SharingGRDB/Documentation.docc/SharingGRDB.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8ba93338..4bee794a 100644 --- a/README.md +++ b/README.md @@ -234,10 +234,10 @@ Orders.fetchAll setup rampup duration SQLite (Enlighter-generated) 0 0.144 7.183 Lighter (1.4.10) 0 0.164 8.059 SharingGRDB (0.2.0) 0 0.172 8.511 - GRDB (7.4.0, manual decoding) 0 0.376 18.819 + GRDB (7.4.1, manual decoding) 0 0.376 18.819 SQLite.swift (0.15.3, manual decoding) 0 0.564 27.994 SQLite.swift (0.15.3, Codable) 0 0.863 43.261 - GRDB (7.4.0, Codable) 0.002 1.07 53.326 + GRDB (7.4.1, Codable) 0.002 1.07 53.326 ``` ## SQLite knowledge required diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index a39162a4..bed31367 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -158,10 +158,10 @@ Orders.fetchAll setup rampup duration SQLite (Enlighter-generated) 0 0.144 7.183 Lighter (1.4.10) 0 0.164 8.059 SharingGRDB (0.2.0) 0 0.172 8.511 - GRDB (7.4.0, manual decoding) 0 0.376 18.819 + GRDB (7.4.1, manual decoding) 0 0.376 18.819 SQLite.swift (0.15.3, manual decoding) 0 0.564 27.994 SQLite.swift (0.15.3, Codable) 0 0.863 43.261 - GRDB (7.4.0, Codable) 0.002 1.07 53.326 + GRDB (7.4.1, Codable) 0.002 1.07 53.326 ``` ## SQLite knowledge required From 6f9cd4c8db50d2ffe26b94fdd418cd714133f15c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 11:13:40 -0700 Subject: [PATCH 129/171] wip --- README.md | 2 +- Sources/SharingGRDB/Documentation.docc/SharingGRDB.md | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4bee794a..1ebdb330 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ SQL queries, including joins and aggregates, and performance, including indices. With some basic knowledge you can apply this library to your database schema in order to query for data and keep your views up-to-date when data in the database changes, and you can use [StructuredQueries][structured-queries-gh] to build queries, either using its type-safe, -discoverable query builder, or using its `#sql` macro for writing safe SQL strings. +discoverable query building APIs, or using its `#sql` macro for writing safe SQL strings. [query-interface]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/queryinterface [sharing-gh]: https://github.com/pointfreeco/swift-sharing diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index bed31367..c51bf27a 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -174,11 +174,10 @@ library you should be familiar with the basics of SQLite, including schema desig SQL queries, including joins and aggregates, and performance, including indices. With some basic knowledge you can apply this library to your database schema in order to query -for data and keep your views up-to-date when data in the database changes. You can use GRDB's -[query builder][query-interface] APIs to query your database, or you can use raw SQL queries, -along with all of the power that SQL has to offer. - -[query-interface]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/queryinterface +for data and keep your views up-to-date when data in the database changes, and you can use +[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to build queries, +either using its type-safe, discoverable query building APIs, or using its `#sql` macro for writing +safe SQL strings. ## What is Sharing? From 7b5c52f2931cd50c5eb9d8d209ece7e5097308c5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 11:14:57 -0700 Subject: [PATCH 130/171] wip --- README.md | 4 ++-- Sources/SharingGRDB/Documentation.docc/SharingGRDB.md | 2 +- .../Documentation.docc/StructuredQueriesGRDB.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1ebdb330..9a9d177d 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ the [articles][articles] below to learn how to best utilize this library: [comparison-swiftdata-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/comparisonwithswiftdata [fetching-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/fetching [preparing-db-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/preparingdatabase - [fetchall-docs]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/sharing/sharedreaderkey/fetchall(sql:arguments:database:animation:) +[fetchall-docs]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/sharing/sharedreaderkey/fetchall(sql:arguments:database:animation:) ## Performance @@ -243,7 +243,7 @@ Orders.fetchAll setup rampup duration ## SQLite knowledge required SQLite is one of the - [most established and widely distributed](https://www.sqlite.org/mostdeployed.html) pieces of +[most established and widely distributed](https://www.sqlite.org/mostdeployed.html) pieces of software in the history of software. Knowledge of SQLite is a great skill for any app developer to have, and this library does not want to conceal it from you. So, we feel that to best wield this library you should be familiar with the basics of SQLite, including schema design and normalization, diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index c51bf27a..eb015866 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -167,7 +167,7 @@ Orders.fetchAll setup rampup duration ## SQLite knowledge required SQLite is one of the - [most established and widely distributed](https://www.sqlite.org/mostdeployed.html) pieces of +[most established and widely distributed](https://www.sqlite.org/mostdeployed.html) pieces of software in the history of software. Knowledge of SQLite is a great skill for any app developer to have, and this library does not want to conceal it from you. So, we feel that to best wield this library you should be familiar with the basics of SQLite, including schema design and normalization, diff --git a/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md b/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md index e6ad5405..1a7866da 100644 --- a/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md +++ b/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md @@ -5,4 +5,4 @@ A library interfacing StructuredQueries with GRDB. ## Overview The core functionality of this library is defined in - [`StructuredQueriesGRDBCore`](structuredqueriesgrdbcore). +[`StructuredQueriesGRDBCore`](structuredqueriesgrdbcore). From 6a5e5b9e903d03441952d0322187d5239af66f85 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 11:38:16 -0700 Subject: [PATCH 131/171] wip --- README.md | 2 +- Sources/SharingGRDB/Documentation.docc/SharingGRDB.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9a9d177d..515e7bbd 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ SharingGRDB leverages high-performance decoding from [StructuredQueries][structu turn fetched data into your Swift domain types, and has a performance profile similar to invoking SQLite's C APIs directly. -See the following benchmarks from +See the following benchmarks against [Lighter's performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite) for a taste of how it compares: diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index eb015866..171fbcd4 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -149,7 +149,7 @@ SharingGRDB leverages high-performance decoding from into your Swift domain types, and has a performance profile similar to invoking SQLite's C APIs directly. -See the following benchmarks from +See the following benchmarks against [Lighter's performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite) for a taste of how it compares: From a9252cc68086c7b7efe5a67446c47d224352a403 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 11:40:52 -0700 Subject: [PATCH 132/171] wip --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 515e7bbd..d30a11b7 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ Sharing. Check out [this](./Examples) directory to see them all, including: The documentation for releases and `main` are available here: * [`main`](https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/) - * [0.1.x](https://swiftpackageindex.com/pointfreeco/sharing-grdb/~/documentation/sharinggrdb/) + * [0.x.x](https://swiftpackageindex.com/pointfreeco/sharing-grdb/~/documentation/sharinggrdb/) ## Installation @@ -298,7 +298,7 @@ simple as adding it to your `Package.swift`: ``` swift dependencies: [ - .package(url: "https://github.com/pointfreeco/sharing-grdb", from: "0.1.0") + .package(url: "https://github.com/pointfreeco/sharing-grdb", from: "0.2.0") ] ``` From 158ab085aab7906369b6ef072c38aa7b7bc6c5af Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 11:52:45 -0700 Subject: [PATCH 133/171] wip --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d30a11b7..d4d4dba4 100644 --- a/README.md +++ b/README.md @@ -302,12 +302,18 @@ dependencies: [ ] ``` -And then adding the product to any target that needs access to the library: +And then adding the following products to any target that needs access to the library: ```swift .product(name: "SharingGRDB", package: "sharing-grdb"), +.product(name: "StructuredQueriesGRDB", package: "sharing-grdb"), ``` +> [!NOTE] +> The `SharingGRDB` module contains code for using `@SharedReader`, which is equivalent to +> SwiftData's `@Query`, while `StructuredQueriesGRDB` contains code for using `@Table`, which is +> equivalent to SwiftData's `@Model`. + ## Community If you want to discuss this library or have a question about how to use it to solve a particular From 009453c4f2eafdde55b96fa648bc477a7c227227 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 13:21:08 -0700 Subject: [PATCH 134/171] wip --- .spi.yml | 1 + Examples/CaseStudies/Animations.swift | 1 - Examples/CaseStudies/DynamicQuery.swift | 1 - .../CaseStudies/ObservableModelDemo.swift | 1 - .../CaseStudies/SwiftDataTemplateDemo.swift | 1 - Examples/CaseStudies/SwiftUIDemo.swift | 1 - Examples/CaseStudies/TransactionDemo.swift | 1 - Examples/CaseStudies/UIKitDemo.swift | 1 - Examples/Reminders/ReminderForm.swift | 1 - Examples/Reminders/RemindersListDetail.swift | 1 - Examples/Reminders/RemindersListRow.swift | 1 - Examples/Reminders/Schema.swift | 1 - Examples/SyncUps/Schema.swift | 1 - Package.swift | 12 +- README.md | 8 +- .../Documentation.docc/SharingGRDB.md | 244 +----------------- Sources/SharingGRDB/Exports.swift | 2 + .../Articles/ComparisonWithSwiftData.md | 0 .../Articles/DynamicQueries.md | 0 .../Documentation.docc/Articles/Fetching.md | 0 .../Documentation.docc/Articles/Observing.md | 0 .../Articles/PreparingDatabase.md | 0 .../Documentation.docc/Extensions/Fetch.md | 0 .../Documentation.docc/Extensions/FetchAll.md | 0 .../Documentation.docc/Extensions/FetchOne.md | 0 .../Documentation.docc/SharingGRDBCore.md | 241 +++++++++++++++++ .../FetchKey+SwiftUI.swift | 0 .../FetchKey.swift | 0 .../FetchKeyRequest.swift | 0 .../Internal/Exports.swift | 0 .../StructuredQueries/StatementKey.swift | 0 .../StructuredQueriesGRDB.md | 8 +- .../StructuredQueriesGRDBCore.md | 2 +- 33 files changed, 276 insertions(+), 254 deletions(-) create mode 100644 Sources/SharingGRDB/Exports.swift rename Sources/{SharingGRDB => SharingGRDBCore}/Documentation.docc/Articles/ComparisonWithSwiftData.md (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/Documentation.docc/Articles/DynamicQueries.md (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/Documentation.docc/Articles/Fetching.md (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/Documentation.docc/Articles/Observing.md (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/Documentation.docc/Articles/PreparingDatabase.md (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/Documentation.docc/Extensions/Fetch.md (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/Documentation.docc/Extensions/FetchAll.md (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/Documentation.docc/Extensions/FetchOne.md (100%) create mode 100644 Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md rename Sources/{SharingGRDB => SharingGRDBCore}/FetchKey+SwiftUI.swift (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/FetchKey.swift (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/FetchKeyRequest.swift (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/Internal/Exports.swift (100%) rename Sources/{SharingGRDB => SharingGRDBCore}/StructuredQueries/StatementKey.swift (100%) diff --git a/.spi.yml b/.spi.yml index 9dabcfa4..2de39d36 100644 --- a/.spi.yml +++ b/.spi.yml @@ -3,6 +3,7 @@ builder: configs: - documentation_targets: - SharingGRDB + - SharingGRDBCore - StructuredQueriesGRDB - StructuredQueriesGRDBCore swift_version: 6.0 diff --git a/Examples/CaseStudies/Animations.swift b/Examples/CaseStudies/Animations.swift index 15960d43..180c8a2b 100644 --- a/Examples/CaseStudies/Animations.swift +++ b/Examples/CaseStudies/Animations.swift @@ -1,6 +1,5 @@ import Dependencies import SharingGRDB -import StructuredQueriesGRDB import SwiftUI struct AnimationsCaseStudy: SwiftUICaseStudy { diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index 9d3b0b18..43eda9ef 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -1,6 +1,5 @@ import Dependencies import SharingGRDB -import StructuredQueriesGRDB import SwiftUI struct DynamicQueryDemo: SwiftUICaseStudy { diff --git a/Examples/CaseStudies/ObservableModelDemo.swift b/Examples/CaseStudies/ObservableModelDemo.swift index 1af02aa9..984df9ff 100644 --- a/Examples/CaseStudies/ObservableModelDemo.swift +++ b/Examples/CaseStudies/ObservableModelDemo.swift @@ -1,6 +1,5 @@ import Dependencies import SharingGRDB -import StructuredQueriesGRDB import SwiftUI struct ObservableModelDemo: SwiftUICaseStudy { diff --git a/Examples/CaseStudies/SwiftDataTemplateDemo.swift b/Examples/CaseStudies/SwiftDataTemplateDemo.swift index 3c394bc9..2d120749 100644 --- a/Examples/CaseStudies/SwiftDataTemplateDemo.swift +++ b/Examples/CaseStudies/SwiftDataTemplateDemo.swift @@ -1,5 +1,4 @@ import SharingGRDB -import StructuredQueriesGRDB import SwiftUI struct SwiftDataTemplateView: SwiftUICaseStudy { diff --git a/Examples/CaseStudies/SwiftUIDemo.swift b/Examples/CaseStudies/SwiftUIDemo.swift index 6ebeb7b4..c3933424 100644 --- a/Examples/CaseStudies/SwiftUIDemo.swift +++ b/Examples/CaseStudies/SwiftUIDemo.swift @@ -1,6 +1,5 @@ import Dependencies import SharingGRDB -import StructuredQueriesGRDB import SwiftUI struct SwiftUIDemo: SwiftUICaseStudy { diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index 9bec74f4..5da8c54a 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -1,6 +1,5 @@ import Dependencies import SharingGRDB -import StructuredQueriesGRDB import SwiftUI struct TransactionDemo: SwiftUICaseStudy { diff --git a/Examples/CaseStudies/UIKitDemo.swift b/Examples/CaseStudies/UIKitDemo.swift index d0a38664..93951d25 100644 --- a/Examples/CaseStudies/UIKitDemo.swift +++ b/Examples/CaseStudies/UIKitDemo.swift @@ -1,5 +1,4 @@ import SharingGRDB -import StructuredQueriesGRDB import SwiftNavigation import SwiftUI import UIKit diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 8d9e42d8..f7e9d874 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -2,7 +2,6 @@ import Dependencies import GRDB import IssueReporting import SharingGRDB -import StructuredQueriesGRDB import SwiftUI struct ReminderFormView: View { diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 202b101f..8568a125 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -1,7 +1,6 @@ import CasePaths import Sharing import SharingGRDB -import StructuredQueriesGRDB import SwiftUI struct RemindersListDetailView: View { diff --git a/Examples/Reminders/RemindersListRow.swift b/Examples/Reminders/RemindersListRow.swift index b29279b3..59f3e829 100644 --- a/Examples/Reminders/RemindersListRow.swift +++ b/Examples/Reminders/RemindersListRow.swift @@ -1,5 +1,4 @@ import SharingGRDB -import StructuredQueriesGRDB import SwiftUI struct RemindersListRow: View { diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index bcae27cc..e093658c 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -2,7 +2,6 @@ import Foundation import GRDB import IssueReporting import SharingGRDB -import StructuredQueriesGRDB import SwiftUI @Table diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 22728c88..cd5db217 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -1,5 +1,4 @@ import SharingGRDB -import StructuredQueriesGRDB import SwiftUI @Table diff --git a/Package.swift b/Package.swift index 4ec8fb0e..914dfa5a 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,10 @@ let package = Package( name: "SharingGRDB", targets: ["SharingGRDB"] ), + .library( + name: "SharingGRDBCore", + targets: ["SharingGRDBCore"] + ), .library( name: "StructuredQueriesGRDB", targets: ["StructuredQueriesGRDB"] @@ -34,6 +38,13 @@ let package = Package( targets: [ .target( name: "SharingGRDB", + dependencies: [ + "SharingGRDBCore", + "StructuredQueriesGRDB", + ] + ), + .target( + name: "SharingGRDBCore", dependencies: [ "StructuredQueriesGRDBCore", .product(name: "GRDB", package: "GRDB.swift"), @@ -48,7 +59,6 @@ let package = Package( .product(name: "StructuredQueries", package: "swift-structured-queries"), ] ), - .target( name: "StructuredQueriesGRDBCore", dependencies: [ diff --git a/README.md b/README.md index d4d4dba4..bb6e4abf 100644 --- a/README.md +++ b/README.md @@ -302,18 +302,12 @@ dependencies: [ ] ``` -And then adding the following products to any target that needs access to the library: +And then adding the following product to any target that needs access to the library: ```swift .product(name: "SharingGRDB", package: "sharing-grdb"), -.product(name: "StructuredQueriesGRDB", package: "sharing-grdb"), ``` -> [!NOTE] -> The `SharingGRDB` module contains code for using `@SharedReader`, which is equivalent to -> SwiftData's `@Query`, while `StructuredQueriesGRDB` contains code for using `@Table`, which is -> equivalent to SwiftData's `@Model`. - ## Community If you want to discuss this library or have a question about how to use it to solve a particular diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index 171fbcd4..f74decfe 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -1,238 +1,22 @@ # ``SharingGRDB`` -## Overview - -SharingGRDB is lightweight replacement for SwiftData and the `@Query` macro that deploys all the way -back to the iOS 13 generation of targets. - -@Row { - @Column { - ```swift - // SharingGRDB - @SharedReader(.fetchAll(Item.all)) - var items - - @Table - struct Item { - let id: Int - var title = "" - var isInStock = true - var notes = "" - } - ``` - } - @Column { - ```swift - // SwiftData - @Query - var items: [Item] - - @Model - class Item { - var title: String - var isInStock: Bool - var notes: String - init( - title: String = "", - isInStock: Bool = true, - notes: String = "" - ) { - self.title = title - self.isInStock = isInStock - self.notes = notes - } - } - ``` - } -} - -Both of the above examples fetch items from an external data store using Swift data types, and both -are automatically observed by SwiftUI so that views are recomputed when the external data changes, -but SharingGRDB is powered directly by SQLite using [Sharing](#What-is-Sharing), -[StructuredQueries](#What-is-StructuredQueries), and [GRDB](#What-is-GRDB), and is usable from -anywhere, including UIKit, `@Observable` models, and more. - -> Note: For more information on SharingGRDB's querying capabilities, see . - -## Quick start - -Before SharingGRDB's property wrappers can fetch data from SQLite, you need to provide---at -runtime---the default database it should use. This is typically done as early as possible in your -app's lifetime, like the app entry point in SwiftUI, and is analogous to configuring model storage -in SwiftData: - -@Row { - @Column { - ```swift - // SharingGRDB - @main - struct MyApp: App { - init() { - prepareDependencies { - // Create/migrate a database connection - let db = try! DatabaseQueue(/* ... */) - $0.defaultDatabase = db - } - } - // ... - } - ``` - } - @Column { - ```swift - // SwiftData - @main - struct MyApp: App { - let container = { - // Create/configure a container - try! ModelContainer(/* ... */) - }() - - var body: some Scene { - WindowGroup { - ContentView() - .modelContainer(container) - } - } - } - ``` - } -} - -> Note: For more information on preparing a SQLite database, see . - -This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies, like -[`fetchAll`]( Note: For more information on how SharingGRDB compares to SwiftData, see -> . - -This is all you need to know to get started with SharingGRDB, but there's much more to learn. Read -the [articles](#Essentials) below to learn how to best utilize this library. - -## Performance +A fast, lightweight replacement for SwiftData, powered by SQL. -SharingGRDB leverages high-performance decoding from -[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to turn fetched data -into your Swift domain types, and has a performance profile similar to invoking SQLite's C APIs -directly. - -See the following benchmarks against -[Lighter's performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite) for a -taste of how it compares: - -``` -Orders.fetchAll setup rampup duration - SQLite (Enlighter-generated) 0 0.144 7.183 - Lighter (1.4.10) 0 0.164 8.059 - SharingGRDB (0.2.0) 0 0.172 8.511 - GRDB (7.4.1, manual decoding) 0 0.376 18.819 - SQLite.swift (0.15.3, manual decoding) 0 0.564 27.994 - SQLite.swift (0.15.3, Codable) 0 0.863 43.261 - GRDB (7.4.1, Codable) 0.002 1.07 53.326 -``` - -## SQLite knowledge required - -SQLite is one of the -[most established and widely distributed](https://www.sqlite.org/mostdeployed.html) pieces of -software in the history of software. Knowledge of SQLite is a great skill for any app developer to -have, and this library does not want to conceal it from you. So, we feel that to best wield this -library you should be familiar with the basics of SQLite, including schema design and normalization, -SQL queries, including joins and aggregates, and performance, including indices. - -With some basic knowledge you can apply this library to your database schema in order to query -for data and keep your views up-to-date when data in the database changes, and you can use -[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to build queries, -either using its type-safe, discoverable query building APIs, or using its `#sql` macro for writing -safe SQL strings. - -## What is Sharing? - -[Sharing](https://github.com/pointfreeco/swift-sharing) is a universal and extensible solution for -sharing your app's model data across features and with external systems, such as user defaults, -the file system, and more. This library builds upon the tools from Sharing in order to allow for -the [fetching]() and [observing]() of data in a SQLite database. - -This is all you need to know about Sharing to hit the ground running with SharingGRDB, but it only -scratches the surface of what the library makes possible. It can also act as a replacement to -SwiftUI's `@AppStorage` that works with UIKit and `@Observable` models, and can be integrated -with custom persistence strategies. To learn more, check out -[the documentation](https://swiftpackageindex.com/pointfreeco/swift-sharing/~/documentation/sharing/). - -## What is StructuredQueries? - -[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) is a library for -building SQL in a safe, expressive, and composable manner, and decoding results with high -performance. Learn more about designing schemas and building queries with the library by seeing its -[documentation](https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/). - -SharingGRDB contains an official StructuredQueries driver that connects it to SQLite _via_ GRDB, -though its query builder and decoder are general purpose tools that can interface with other -databases (MySQL, Postgres, _etc._) and database libraries. - -## What is GRDB? - -[GRDB](https://github.com/groue/GRDB.swift) is a popular Swift interface to SQLite with a rich -feature set and -[extensive documentation](https://swiftpackageindex.com/groue/GRDB.swift/documentation/grdb). -This library leverages GRDB's' observation APIs to keep the `@SharedReader` property wrapper in -sync with the database and update SwiftUI views. - -If you're already familiar with SQLite, GRDB provides thin APIs that can be leveraged with raw SQL -in short order. If you're new to SQLite, GRDB offers a great introduction to a highly portable -database engine. We recommend -([as does GRDB](https://github.com/groue/GRDB.swift?tab=readme-ov-file#documentation)) a familiarity -with SQLite to take full advantage of GRDB and SharingGRDB. - -## Topics - -### Essentials +## Overview -- -- -- -- -- +The core functionality of this library is defined in +[`SharingGRDBCore`](sharinggrdbcore) and [`StructuredQueriesGRDBCore`](structuredquereisgrdbcore), +which this module automatically exports. -### Database configuration and access +> Note: This module also exports `StructuredQueries`, which provides the `@Table` macro for building +> and decoding queries. If you are using [GRDB][]'s built-in tools instead of +> [StructuredQueries][], consider depending on `SharingGRDBCore`, instead. -- ``Dependencies/DependencyValues/defaultDatabase`` +See [`SharingGRDBCore`](sharinggrdbcore) for documentation on the integration with the +`@SharedReader` property wrapper, which is equivalent to SwiftData's `@Query`. -### Fetch strategies +See [`StructuredQueriesGRDBCore`](sharinggrdbcore) for documentation on the integration between +[StructuredQueries][] and [GRDB][]. -- ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` -- ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:)`` -- ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` +[GRDB]: https://github.com/groue/GRDB.swift +[StructuredQueries]: https://github.com/pointfreeco/swift-structured-queries diff --git a/Sources/SharingGRDB/Exports.swift b/Sources/SharingGRDB/Exports.swift new file mode 100644 index 00000000..07a5c659 --- /dev/null +++ b/Sources/SharingGRDB/Exports.swift @@ -0,0 +1,2 @@ +@_exported import SharingGRDBCore +@_exported import StructuredQueriesGRDB diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md similarity index 100% rename from Sources/SharingGRDB/Documentation.docc/Articles/ComparisonWithSwiftData.md rename to Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/DynamicQueries.md similarity index 100% rename from Sources/SharingGRDB/Documentation.docc/Articles/DynamicQueries.md rename to Sources/SharingGRDBCore/Documentation.docc/Articles/DynamicQueries.md diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md similarity index 100% rename from Sources/SharingGRDB/Documentation.docc/Articles/Fetching.md rename to Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/Observing.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/Observing.md similarity index 100% rename from Sources/SharingGRDB/Documentation.docc/Articles/Observing.md rename to Sources/SharingGRDBCore/Documentation.docc/Articles/Observing.md diff --git a/Sources/SharingGRDB/Documentation.docc/Articles/PreparingDatabase.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md similarity index 100% rename from Sources/SharingGRDB/Documentation.docc/Articles/PreparingDatabase.md rename to Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md diff --git a/Sources/SharingGRDB/Documentation.docc/Extensions/Fetch.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md similarity index 100% rename from Sources/SharingGRDB/Documentation.docc/Extensions/Fetch.md rename to Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md diff --git a/Sources/SharingGRDB/Documentation.docc/Extensions/FetchAll.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md similarity index 100% rename from Sources/SharingGRDB/Documentation.docc/Extensions/FetchAll.md rename to Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md diff --git a/Sources/SharingGRDB/Documentation.docc/Extensions/FetchOne.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md similarity index 100% rename from Sources/SharingGRDB/Documentation.docc/Extensions/FetchOne.md rename to Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md diff --git a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md new file mode 100644 index 00000000..535cd5bc --- /dev/null +++ b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md @@ -0,0 +1,241 @@ +# ``SharingGRDBCore`` + +A fast, lightweight replacement for SwiftData, powered by SQL. This module is automatically imported +when you `import SharingGRDB`. + +## Overview + +SharingGRDB is [fast](#Performance), lightweight replacement for SwiftData that deploys all the way +back to the iOS 13 generation of targets. + +@Row { + @Column { + ```swift + // SharingGRDB + @SharedReader(.fetchAll(Item.all)) + var items + + @Table + struct Item { + let id: Int + var title = "" + var isInStock = true + var notes = "" + } + ``` + } + @Column { + ```swift + // SwiftData + @Query + var items: [Item] + + @Model + class Item { + var title: String + var isInStock: Bool + var notes: String + init( + title: String = "", + isInStock: Bool = true, + notes: String = "" + ) { + self.title = title + self.isInStock = isInStock + self.notes = notes + } + } + ``` + } +} + +Both of the above examples fetch items from an external data store using Swift data types, and both +are automatically observed by SwiftUI so that views are recomputed when the external data changes, +but SharingGRDB is powered directly by SQLite using [Sharing](#What-is-Sharing), +[StructuredQueries](#What-is-StructuredQueries), and [GRDB](#What-is-GRDB), and is usable from +anywhere, including UIKit, `@Observable` models, and more. + +> Note: For more information on SharingGRDB's querying capabilities, see . + +## Quick start + +Before SharingGRDB's property wrappers can fetch data from SQLite, you need to provide---at +runtime---the default database it should use. This is typically done as early as possible in your +app's lifetime, like the app entry point in SwiftUI, and is analogous to configuring model storage +in SwiftData: + +@Row { + @Column { + ```swift + // SharingGRDB + @main + struct MyApp: App { + init() { + prepareDependencies { + // Create/migrate a database connection + let db = try! DatabaseQueue(/* ... */) + $0.defaultDatabase = db + } + } + // ... + } + ``` + } + @Column { + ```swift + // SwiftData + @main + struct MyApp: App { + let container = { + // Create/configure a container + try! ModelContainer(/* ... */) + }() + + var body: some Scene { + WindowGroup { + ContentView() + .modelContainer(container) + } + } + } + ``` + } +} + +> Note: For more information on preparing a SQLite database, see . + +This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies, like +[`fetchAll`]( Note: For more information on how SharingGRDB compares to SwiftData, see +> . + +This is all you need to know to get started with SharingGRDB, but there's much more to learn. Read +the [articles](#Essentials) below to learn how to best utilize this library. + +## Performance + +SharingGRDB leverages high-performance decoding from +[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to turn fetched data +into your Swift domain types, and has a performance profile similar to invoking SQLite's C APIs +directly. + +See the following benchmarks against +[Lighter's performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite) for a +taste of how it compares: + +``` +Orders.fetchAll setup rampup duration + SQLite (Enlighter-generated) 0 0.144 7.183 + Lighter (1.4.10) 0 0.164 8.059 + SharingGRDB (0.2.0) 0 0.172 8.511 + GRDB (7.4.1, manual decoding) 0 0.376 18.819 + SQLite.swift (0.15.3, manual decoding) 0 0.564 27.994 + SQLite.swift (0.15.3, Codable) 0 0.863 43.261 + GRDB (7.4.1, Codable) 0.002 1.07 53.326 +``` + +## SQLite knowledge required + +SQLite is one of the +[most established and widely distributed](https://www.sqlite.org/mostdeployed.html) pieces of +software in the history of software. Knowledge of SQLite is a great skill for any app developer to +have, and this library does not want to conceal it from you. So, we feel that to best wield this +library you should be familiar with the basics of SQLite, including schema design and normalization, +SQL queries, including joins and aggregates, and performance, including indices. + +With some basic knowledge you can apply this library to your database schema in order to query +for data and keep your views up-to-date when data in the database changes, and you can use +[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) to build queries, +either using its type-safe, discoverable query building APIs, or using its `#sql` macro for writing +safe SQL strings. + +## What is Sharing? + +[Sharing](https://github.com/pointfreeco/swift-sharing) is a universal and extensible solution for +sharing your app's model data across features and with external systems, such as user defaults, +the file system, and more. This library builds upon the tools from Sharing in order to allow for +the [fetching]() and [observing]() of data in a SQLite database. + +This is all you need to know about Sharing to hit the ground running with SharingGRDB, but it only +scratches the surface of what the library makes possible. It can also act as a replacement to +SwiftUI's `@AppStorage` that works with UIKit and `@Observable` models, and can be integrated +with custom persistence strategies. To learn more, check out +[the documentation](https://swiftpackageindex.com/pointfreeco/swift-sharing/~/documentation/sharing/). + +## What is StructuredQueries? + +[StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) is a library for +building SQL in a safe, expressive, and composable manner, and decoding results with high +performance. Learn more about designing schemas and building queries with the library by seeing its +[documentation](https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/). + +SharingGRDB contains an official StructuredQueries driver that connects it to SQLite _via_ GRDB, +though its query builder and decoder are general purpose tools that can interface with other +databases (MySQL, Postgres, _etc._) and database libraries. + +## What is GRDB? + +[GRDB](https://github.com/groue/GRDB.swift) is a popular Swift interface to SQLite with a rich +feature set and +[extensive documentation](https://swiftpackageindex.com/groue/GRDB.swift/documentation/grdb). +This library leverages GRDB's' observation APIs to keep the `@SharedReader` property wrapper in +sync with the database and update SwiftUI views. + +If you're already familiar with SQLite, GRDB provides thin APIs that can be leveraged with raw SQL +in short order. If you're new to SQLite, GRDB offers a great introduction to a highly portable +database engine. We recommend +([as does GRDB](https://github.com/groue/GRDB.swift?tab=readme-ov-file#documentation)) a familiarity +with SQLite to take full advantage of GRDB and SharingGRDB. + +## Topics + +### Essentials + +- +- +- +- +- + +### Database configuration and access + +- ``Dependencies/DependencyValues/defaultDatabase`` + +### Fetch strategies + +- ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` +- ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:)`` +- ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` diff --git a/Sources/SharingGRDB/FetchKey+SwiftUI.swift b/Sources/SharingGRDBCore/FetchKey+SwiftUI.swift similarity index 100% rename from Sources/SharingGRDB/FetchKey+SwiftUI.swift rename to Sources/SharingGRDBCore/FetchKey+SwiftUI.swift diff --git a/Sources/SharingGRDB/FetchKey.swift b/Sources/SharingGRDBCore/FetchKey.swift similarity index 100% rename from Sources/SharingGRDB/FetchKey.swift rename to Sources/SharingGRDBCore/FetchKey.swift diff --git a/Sources/SharingGRDB/FetchKeyRequest.swift b/Sources/SharingGRDBCore/FetchKeyRequest.swift similarity index 100% rename from Sources/SharingGRDB/FetchKeyRequest.swift rename to Sources/SharingGRDBCore/FetchKeyRequest.swift diff --git a/Sources/SharingGRDB/Internal/Exports.swift b/Sources/SharingGRDBCore/Internal/Exports.swift similarity index 100% rename from Sources/SharingGRDB/Internal/Exports.swift rename to Sources/SharingGRDBCore/Internal/Exports.swift diff --git a/Sources/SharingGRDB/StructuredQueries/StatementKey.swift b/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift similarity index 100% rename from Sources/SharingGRDB/StructuredQueries/StatementKey.swift rename to Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift diff --git a/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md b/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md index 1a7866da..c62671c0 100644 --- a/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md +++ b/Sources/StructuredQueriesGRDB/Documentation.docc/StructuredQueriesGRDB.md @@ -1,8 +1,10 @@ # ``StructuredQueriesGRDB`` -A library interfacing StructuredQueries with GRDB. +A library interfacing StructuredQueries with GRDB. This module is automatically imported when you +`import SharingGRDB`. ## Overview -The core functionality of this library is defined in -[`StructuredQueriesGRDBCore`](structuredqueriesgrdbcore). +The core functionality of this module is defined in +[`StructuredQueriesGRDBCore`](structuredqueriesgrdbcore) and then re-exported alongside +`StructuredQueries` and its macros. diff --git a/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md index b947a67a..aa1e1567 100644 --- a/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md +++ b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md @@ -1,7 +1,7 @@ # ``StructuredQueriesGRDBCore`` The core functionality of interfacing StructuredQueries with GRDB. This module is automatically -imported when you `import StructuredQueriesGRDB`. +imported when you `import SharingGRDB` or `StructuredQueriesGRDB`. ## Overview From 0c0f7d36a12a211fa488eab781037a328d49e503 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 14:47:57 -0700 Subject: [PATCH 135/171] wip --- .spi.yml | 2 +- .../Articles/ComparisonWithSwiftData.md | 14 ++- .../Documentation.docc/Articles/Fetching.md | 4 +- .../Articles/PreparingDatabase.md | 13 ++- .../Documentation.docc/Extensions/Fetch.md | 12 +-- .../Documentation.docc/Extensions/FetchAll.md | 6 +- .../Extensions/FetchAllSQL.md | 13 +++ .../Documentation.docc/Extensions/FetchKey.md | 8 ++ .../Extensions/FetchKeyRequest.md | 11 ++ .../Documentation.docc/Extensions/FetchOne.md | 6 +- .../Extensions/FetchOneSQL.md | 13 +++ .../Documentation.docc/SharingGRDBCore.md | 15 ++- .../SharingGRDBCore/FetchKey+SwiftUI.swift | 18 +--- Sources/SharingGRDBCore/FetchKey.swift | 61 +++++------ Sources/SharingGRDBCore/FetchKeyRequest.swift | 4 +- .../StructuredQueries/StatementKey.swift | 101 ++++++++---------- 16 files changed, 160 insertions(+), 141 deletions(-) create mode 100644 Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAllSQL.md create mode 100644 Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchKey.md create mode 100644 Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchKeyRequest.md create mode 100644 Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOneSQL.md diff --git a/.spi.yml b/.spi.yml index 2de39d36..a75857c7 100644 --- a/.spi.yml +++ b/.spi.yml @@ -6,4 +6,4 @@ builder: - SharingGRDBCore - StructuredQueriesGRDB - StructuredQueriesGRDBCore - swift_version: 6.0 + custom_documentation_parameters: [--enable-experimental-overloaded-symbol-presentation] diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md index c2e068dd..3e344239 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -77,8 +77,8 @@ Some key differences: Both SharingGRDB and SwiftData require some work to be done at the entry point of the app in order to set up the external storage system that will be used throughout the app. In SharingGRDB we use -the `prepareDependencies` function to set up the ``Dependencies/DependencyValues/defaultDatabase`` -used, and in SwiftUI you construct a `ModelContainer` and propagate it through the environment: +the `prepareDependencies` function to set up the default database used, and in SwiftUI you construct +a `ModelContainer` and propagate it through the environment: @Row { @Column { @@ -318,13 +318,11 @@ See for more information on how to execute dynamic queries ### Creating, update and delete data -To create, update and delete data from the database you must use the -``Dependencies/DependencyValues/defaultDatabase`` dependency. This is similar to what one does -with SwiftData too, where all changes to the database go through the `ModelContext` and is not -done through the `@Query` macro at all. +To create, update and delete data from the database you must use the `defaultDatabase` dependency. +This is similar to what one does with SwiftData too, where all changes to the database go through +the `ModelContext` and is not done through the `@Query` macro at all. -For example, to get access to the ``Dependencies/DependencyValues/defaultDatabase``, you use the -`@Dependency` property wrapper: +For example, to get access to `defaultDatabase`, you use the `@Dependency` property wrapper: @Row { @Column { diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md index 3e3de684..e66a156f 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md @@ -144,8 +144,8 @@ data we want to query for in a single transaction, and then we can construct it the ``FetchKeyRequest/fetch(_:)`` method. With this conformance defined we can use -[`fetch`]() key to execute the query specified -by the `Items` type, and we can access the `inStockItems` and `itemsCount` properties to get to the +[`fetch`]() key to execute the query specified by +the `Items` type, and we can access the `inStockItems` and `itemsCount` properties to get to the queried data: ```swift diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md index bf463e8d..44a0994a 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md @@ -225,8 +225,8 @@ func appDatabase() throws -> any DatabaseWriter { ### Step 5: Set database connection in entry point Once you have defined your `appDatabase` helper for creating a database connection, you must set -it as the ``Dependencies/DependencyValues/defaultDatabase`` for your app in its entry point. This -can be in done in SwiftUI by using `prepareDependencies` in the `init` of your `App` conformance: +it as the `defaultDatabase` for your app in its entry point. This can be in done in SwiftUI by using +`prepareDependencies` in the `init` of your `App` conformance: ```swift import SharingGRDB @@ -246,8 +246,8 @@ struct MyApp: App { > Important: You can only prepare the default database a single time in the lifetime of your > application. It is best to do this as early as possible after the app launches. -If using app or scene delegates, then you can prepare the -``Dependencies/DependencyValues/defaultDatabase`` in one of those conformances: +If using app or scene delegates, then you can prepare the `defaultDatabase` in one of those +conformances: ```swift import SharingGRDB @@ -263,9 +263,8 @@ class AppDelegate: NSObject, UIApplicationDelegate { } ``` -And if using something besides UIKit or SwiftUI, then simply set the -``Dependencies/DependencyValues/defaultDatabase`` as early as possible in the application's -lifecycle. +And if using something besides UIKit or SwiftUI, then simply set the `defaultDatabase` as early as +possible in the application's lifecycle. It is also important to prepare the database in Xcode previews. This can be done like so: diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md index 75631699..6fbf85ba 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md @@ -1,4 +1,4 @@ -# ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` +# ``Sharing/SharedReaderKey/fetch(_:database:)`` ## Overview @@ -8,19 +8,13 @@ - ``FetchKeyRequest`` -### Collections - -- ``Sharing/SharedReaderKey/fetch(_:database:)-1ee8v`` - ### SwiftUI integration -- ``Sharing/SharedReaderKey/fetch(_:database:animation:)-rgj4`` -- ``Sharing/SharedReaderKey/fetch(_:database:animation:)-j9jb`` +- ``Sharing/SharedReaderKey/fetch(_:database:animation:)`` ### Custom scheduling -- ``Sharing/SharedReaderKey/fetch(_:database:scheduler:)-9arcp`` -- ``Sharing/SharedReaderKey/fetch(_:database:scheduler:)-53u9o`` +- ``Sharing/SharedReaderKey/fetch(_:database:scheduler:)`` ### Sharing infrastructure diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md index 93aa47da..9fdd7ac6 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md @@ -1,4 +1,4 @@ -# ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` +# ``Sharing/SharedReaderKey/fetchAll(_:database:)`` ## Overview @@ -6,8 +6,8 @@ ### SwiftUI integration -- ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:animation:)`` +- ``Sharing/SharedReaderKey/fetchAll(_:database:animation:)`` ### Custom scheduling -- ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:scheduler:)`` +- ``Sharing/SharedReaderKey/fetchAll(_:database:scheduler:)`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAllSQL.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAllSQL.md new file mode 100644 index 00000000..93aa47da --- /dev/null +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAllSQL.md @@ -0,0 +1,13 @@ +# ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` + +## Overview + +## Topics + +### SwiftUI integration + +- ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:animation:)`` + +### Custom scheduling + +- ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:scheduler:)`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchKey.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchKey.md new file mode 100644 index 00000000..49ed8509 --- /dev/null +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchKey.md @@ -0,0 +1,8 @@ +# ``SharingGRDBCore/FetchKey`` + +## Topics + +### Key identity + +- ``FetchKeyID`` +- ``ID-swift.typealias`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchKeyRequest.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchKeyRequest.md new file mode 100644 index 00000000..501238a2 --- /dev/null +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchKeyRequest.md @@ -0,0 +1,11 @@ +# ``SharingGRDBCore/FetchKeyRequest`` + +## Topics + +### Fetch keys + +- ``FetchKey`` + +### Error handling + +- ``NotFound`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md index e20a0b6d..5c4f7307 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md @@ -1,4 +1,4 @@ -# ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:)`` +# ``Sharing/SharedReaderKey/fetchOne(_:database:)`` ## Overview @@ -6,8 +6,8 @@ ### SwiftUI integration -- ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:animation:)`` +- ``Sharing/SharedReaderKey/fetchOne(_:database:animation:)`` ### Custom scheduling -- ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:scheduler:)`` +- ``Sharing/SharedReaderKey/fetchOne(_:database:scheduler:)`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOneSQL.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOneSQL.md new file mode 100644 index 00000000..e20a0b6d --- /dev/null +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOneSQL.md @@ -0,0 +1,13 @@ +# ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:)`` + +## Overview + +## Topics + +### SwiftUI integration + +- ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:animation:)`` + +### Custom scheduling + +- ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:scheduler:)`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md index 535cd5bc..d40fd1ef 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md +++ b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md @@ -5,8 +5,8 @@ when you `import SharingGRDB`. ## Overview -SharingGRDB is [fast](#Performance), lightweight replacement for SwiftData that deploys all the way -back to the iOS 13 generation of targets. +SharingGRDB is a [fast](#Performance), lightweight replacement for SwiftData that deploys all the +way back to the iOS 13 generation of targets. @Row { @Column { @@ -236,6 +236,15 @@ with SQLite to take full advantage of GRDB and SharingGRDB. ### Fetch strategies +- ``Sharing/SharedReaderKey/fetchAll(_:database:)`` +- ``Sharing/SharedReaderKey/fetchOne(_:database:)`` +- ``Sharing/SharedReaderKey/fetch(_:database:)`` + +### Raw SQL strategies + - ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` - ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:)`` -- ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` + +### Custom requests + +- ``FetchKeyRequest`` diff --git a/Sources/SharingGRDBCore/FetchKey+SwiftUI.swift b/Sources/SharingGRDBCore/FetchKey+SwiftUI.swift index 1911cd74..7b7ec499 100644 --- a/Sources/SharingGRDBCore/FetchKey+SwiftUI.swift +++ b/Sources/SharingGRDBCore/FetchKey+SwiftUI.swift @@ -6,9 +6,7 @@ extension SharedReaderKey { /// A key that can query for data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` that can be configured - /// with a SwiftUI animation. See ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` for more - /// info on how to use this API. + /// A version of `fetch` that can be configured with a SwiftUI animation. /// /// - Parameters: /// - request: A request describing the data to fetch. @@ -27,9 +25,7 @@ /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` that can be configured - /// with a SwiftUI animation. See ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` for more - /// info on how to use this API. + /// A version of `fetch` that can be configured with a SwiftUI animation. /// /// - Parameters: /// - request: A request describing the data to fetch. @@ -48,10 +44,7 @@ /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` that can be - /// configured with a SwiftUI animation. See - /// ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` for more information on how to - /// use this API. + /// A version of `fetchAll` that can be configured with a SwiftUI animation. /// /// - Parameters: /// - sql: A raw SQL string describing the data to fetch. @@ -77,10 +70,7 @@ /// A key that can query for a value in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:)`` that can be - /// configured with a SwiftUI animation. See - /// ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` for more information on how to - /// use this API. + /// A version of `fetchOne` that can be configured with a SwiftUI animation. /// /// - Parameters: /// - sql: A raw SQL string describing the data to fetch. diff --git a/Sources/SharingGRDBCore/FetchKey.swift b/Sources/SharingGRDBCore/FetchKey.swift index 073e9d9d..66c90a69 100644 --- a/Sources/SharingGRDBCore/FetchKey.swift +++ b/Sources/SharingGRDBCore/FetchKey.swift @@ -37,13 +37,13 @@ extension SharedReaderKey { /// ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:)``, instead. /// /// To animate or observe changes with a custom scheduler, see - /// ``Sharing/SharedReaderKey/fetch(_:database:animation:)-rgj4`` or - /// ``Sharing/SharedReaderKey/fetch(_:database:scheduler:)-9arcp``. + /// ``Sharing/SharedReaderKey/fetch(_:database:animation:)`` or + /// ``Sharing/SharedReaderKey/fetch(_:database:scheduler:)``. /// /// - Parameters: /// - request: A request describing the data to fetch. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use + /// `@Dependency(\.defaultDatabase)``. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetch( _ request: some FetchKeyRequest, @@ -55,20 +55,18 @@ extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` that allows you to omit the - /// type and default from the `@SharedReader` property wrapper: + /// A version of `fetch` that allows you to omit the type and default from the `@SharedReader` + /// property wrapper: /// /// ```diff /// -@SharedReader(.fetch(Items()) var items: [Item] = [] /// +@SharedReader(.fetch(Items()) var items /// ``` /// - /// See ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` for more info on how to use this API. - /// /// - Parameters: /// - request: A request describing the data to fetch. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use + /// `@Dependency(\.defaultDatabase)``. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetch( _ request: some FetchKeyRequest, @@ -87,13 +85,13 @@ extension SharedReaderKey { /// @SharedReader(.fetchAll(sql: "SELECT * FROM items")) var items: [Item] /// ``` /// - /// For more complex querying needs, see ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd``. + /// For more complex querying needs, see ``Sharing/SharedReaderKey/fetch(_:database:)``. /// /// - Parameters: /// - sql: A raw SQL string describing the data to fetch. /// - arguments: Arguments to bind to the SQL statement. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use + /// `@Dependency(\.defaultDatabase)``. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchAll( sql: String, @@ -116,13 +114,13 @@ extension SharedReaderKey { /// @SharedReader(.fetchOne(sql: "SELECT count(*) FROM items")) var itemsCount = 0 /// ``` /// - /// For more complex querying needs, see ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd``. + /// For more complex querying needs, see ``Sharing/SharedReaderKey/fetch(_:database:)``. /// /// - Parameters: /// - sql: A raw SQL string describing the data to fetch. /// - arguments: Arguments to bind to the SQL statement. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use + /// `@Dependency(\.defaultDatabase)``. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchOne( sql: String, @@ -137,14 +135,14 @@ extension SharedReaderKey { extension SharedReaderKey { /// A key that can query for data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` that can be configured - /// with a scheduler. See ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` for more info on - /// how to use this API. + /// A version of ``Sharing/SharedReaderKey/fetch(_:database:)`` that can be configured with a + /// scheduler. See ``Sharing/SharedReaderKey/fetch(_:database:)`` for more info on how to use this + /// API. /// /// - Parameters: /// - request: A request describing the data to fetch. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use + /// `@Dependency(\.defaultDatabase)``. /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -159,14 +157,14 @@ extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` that can be configured - /// with a scheduler. See ``Sharing/SharedReaderKey/fetch(_:database:)-3qcpd`` for more info on - /// how to use this API. + /// A version of ``Sharing/SharedReaderKey/fetch(_:database:)`` that can be configured with a + /// scheduler. See ``Sharing/SharedReaderKey/fetch(_:database:)`` for more info on how to use this + /// API. /// /// - Parameters: /// - request: A request describing the data to fetch. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use + /// `@Dependency(\.defaultDatabase)``. /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -188,8 +186,8 @@ extension SharedReaderKey { /// - Parameters: /// - sql: A raw SQL string describing the data to fetch. /// - arguments: Arguments to bind to the SQL statement. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use + /// `@Dependency(\.defaultDatabase)``. /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -215,8 +213,8 @@ extension SharedReaderKey { /// - Parameters: /// - sql: A raw SQL string describing the data to fetch. /// - arguments: Arguments to bind to the SQL statement. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use + /// `@Dependency(\.defaultDatabase)``. /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -236,8 +234,7 @@ extension SharedReaderKey { /// You typically do not refer to this type directly, and will use /// [`fetchAll`](), /// [`fetchOne`](), and -/// [`fetch`]() to create instances, -/// instead. +/// [`fetch`]() to create instances, instead. public struct FetchKey: SharedReaderKey { let database: any DatabaseReader let request: any FetchKeyRequest diff --git a/Sources/SharingGRDBCore/FetchKeyRequest.swift b/Sources/SharingGRDBCore/FetchKeyRequest.swift index 580562d1..5f74f56d 100644 --- a/Sources/SharingGRDBCore/FetchKeyRequest.swift +++ b/Sources/SharingGRDBCore/FetchKeyRequest.swift @@ -27,8 +27,8 @@ import GRDB /// ``` /// /// And then can be used with a `@SharedReader` and -/// ``Sharing/SharedReaderKey/fetch(_:database:animation:)-rgj4`` to popular state in a SwiftUI -/// view, `@Observable` model, UIKit view controller, and more: +/// ``Sharing/SharedReaderKey/fetch(_:database:animation:)`` to popular state in a SwiftUI view, +/// `@Observable` model, UIKit view controller, and more: /// /// ```swift /// struct PlayersView: View { diff --git a/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift b/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift index 904c4a39..42441afe 100644 --- a/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift @@ -22,8 +22,8 @@ extension SharedReaderKey { /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchAll( _ statement: S, @@ -48,8 +48,8 @@ extension SharedReaderKey { /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchAll( _ statement: S, @@ -69,8 +69,8 @@ extension SharedReaderKey { /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. public static func fetchOne( _ statement: some StructuredQueriesCore.Statement, @@ -95,11 +95,10 @@ extension SharedReaderKey { /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload - @_documentation(visibility: private) public static func fetchAll( _ statement: S, database: (any DatabaseReader)? = nil @@ -122,11 +121,10 @@ extension SharedReaderKey { /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload - @_documentation(visibility: private) public static func fetchAll< S: StructuredQueriesCore.Statement, V1: QueryRepresentable, @@ -152,11 +150,10 @@ extension SharedReaderKey { /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @_disfavoredOverload - @_documentation(visibility: private) public static func fetchOne( _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, database: (any DatabaseReader)? = nil @@ -171,13 +168,12 @@ extension SharedReaderKey { extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a - /// scheduler. + /// A version of `fetchAll` that can be configured with a scheduler. /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -197,13 +193,12 @@ extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a - /// scheduler. + /// A version of `fetchAll` that can be configured with a scheduler. /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -220,13 +215,12 @@ extension SharedReaderKey { /// A key that can query for a value in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a - /// scheduler. + /// A version of `fetchOne` that can be configured with a scheduler. /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -248,13 +242,12 @@ extension SharedReaderKey { extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a - /// scheduler. + /// A version of `fetchAll` that can be configured with a scheduler. /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -275,13 +268,12 @@ extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a - /// scheduler. + /// A version of `fetchAll` that can be configured with a scheduler. /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -307,13 +299,12 @@ extension SharedReaderKey { /// A key that can query for a value in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a - /// scheduler. + /// A version of `fetchOne` that can be configured with a scheduler. /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -337,13 +328,12 @@ extension SharedReaderKey { extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a - /// SwiftUI animation. + /// A version of `fetchAll` that can be configured with a SwiftUI animation. /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -363,13 +353,12 @@ extension SharedReaderKey { /// A key that can query for a collection of data in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a - /// SwiftUI animation. + /// A version of `fetchAll` that can be configured with a SwiftUI animation. /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -388,13 +377,12 @@ extension SharedReaderKey { /// A key that can query for a collection of value in a SQLite database. /// - /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a - /// SwiftUI animation. + /// A version of `fetchOne` that can be configured with a SwiftUI animation. /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -423,8 +411,8 @@ extension SharedReaderKey { /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -450,8 +438,8 @@ extension SharedReaderKey { /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -477,8 +465,7 @@ extension SharedReaderKey { /// /// - Parameters: /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the - /// ``Dependencies/DependencyValues/defaultDatabase``. + /// - database: The database to read from. A value of `nil` will use the default database. /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. From 491d6304e50d315bc9b077af7fef518521fb831b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Apr 2025 15:31:50 -0700 Subject: [PATCH 136/171] Add fetchOne overloads for SelectStatement --- .../StructuredQueries/StatementKey.swift | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift b/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift index 42441afe..ea83bcc8 100644 --- a/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift @@ -59,6 +59,28 @@ extension SharedReaderKey { fetch(FetchAllStatementValueRequest(statement: statement), database: database) } + /// A key that can query for a value in a SQLite database. + /// + /// This key takes a query built using the StructuredQueries library. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + public static func fetchOne( + _ statement: S, + database: (any DatabaseReader)? = nil + ) -> Self + where + S.QueryValue == (), + S.Joins == (), + Self == FetchKey + { + let statement = statement.selectStar() + return fetchOne(statement, database: database) + } + /// A key that can query for a value in a SQLite database. /// /// This key takes a query built using the StructuredQueries library. @@ -140,6 +162,32 @@ extension SharedReaderKey { fetch(FetchAllStatementPackRequest(statement: statement), database: database) } + /// A key that can query for a value in a SQLite database. + /// + /// This key takes a query built using the StructuredQueries library. + /// + /// ```swift + /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items + /// ``` + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @_disfavoredOverload + public static func fetchOne( + _ statement: S, + database: (any DatabaseReader)? = nil + ) -> Self + where + S.QueryValue == (), + S.Joins == (repeat each J), + Self == FetchKey<(S.From.QueryOutput, repeat (each J).QueryOutput)> + { + fetchOne(statement.selectStar(), database: database) + } + /// A key that can query for a value in a SQLite database. /// /// This key takes a query built using the StructuredQueries library. @@ -213,6 +261,31 @@ extension SharedReaderKey { ) } + /// A key that can query for a value in a SQLite database. + /// + /// A version of `fetchOne` that can be configured with a scheduler. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + public static func fetchOne( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) -> Self + where + S.QueryValue == (), + S.Joins == (), + Self == FetchKey + { + let statement = statement.selectStar() + return fetchOne(statement, database: database, scheduler: scheduler) + } + /// A key that can query for a value in a SQLite database. /// /// A version of `fetchOne` that can be configured with a scheduler. @@ -297,6 +370,32 @@ extension SharedReaderKey { ) } + /// A key that can query for a value in a SQLite database. + /// + /// A version of `fetchOne` that can be configured with a scheduler. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @_disfavoredOverload + @_documentation(visibility: private) + public static func fetchOne( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) -> Self + where + S.QueryValue == (), + S.Joins == (repeat each J), + Self == FetchKey<(S.From.QueryOutput, repeat (each J).QueryOutput)> + { + fetchOne(statement.selectStar(), database: database, scheduler: scheduler) + } + /// A key that can query for a value in a SQLite database. /// /// A version of `fetchOne` that can be configured with a scheduler. @@ -375,6 +474,31 @@ extension SharedReaderKey { ) } + /// A key that can query for a value in a SQLite database. + /// + /// A version of `fetchOne` that can be configured with a SwiftUI animation. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + public static func fetchOne( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) -> Self + where + S.QueryValue == (), + S.Joins == (), + Self == FetchKey + { + let statement = statement.selectStar() + return fetchOne(statement, database: database, animation: animation) + } + /// A key that can query for a collection of value in a SQLite database. /// /// A version of `fetchOne` that can be configured with a SwiftUI animation. @@ -458,6 +582,33 @@ extension SharedReaderKey { ) } + /// A key that can query for a value in a SQLite database. + /// + /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a + /// SwiftUI animation. + /// + /// - Parameters: + /// - statement: A structured query describing the data to be fetched. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @_disfavoredOverload + @_documentation(visibility: private) + public static func fetchOne( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) -> Self + where + S.QueryValue == (), + S.Joins == (repeat each J), + Self == FetchKey<(S.From.QueryOutput, repeat (each J).QueryOutput)> + { + fetchOne(statement.selectStar(), database: database, animation: animation) + } + /// A key that can query for a value in a SQLite database. /// /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a From 95744a7a49efe385ca4232fe6646bbdc4caa50c4 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 19 Apr 2025 12:17:39 -0700 Subject: [PATCH 137/171] Basic support for lists of reminders by tag --- Examples/Reminders/RemindersListDetail.swift | 49 ++++++++++++++------ Examples/Reminders/RemindersLists.swift | 30 ++++++++++++ Examples/Reminders/Schema.swift | 15 ++++-- Examples/Reminders/TagRow.swift | 40 ++++++++++++++++ Examples/Reminders/TagsForm.swift | 5 +- Examples/SyncUps/SyncUpDetail.swift | 2 +- 6 files changed, 118 insertions(+), 23 deletions(-) create mode 100644 Examples/Reminders/TagRow.swift diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 8568a125..5e4832c7 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -148,6 +148,7 @@ struct RemindersListDetailView: View { case flagged case list(RemindersList) case scheduled + case tags([Tag]) case today } @@ -163,16 +164,6 @@ struct RemindersListDetailView: View { !$0.isCompleted } } - .where { - switch detailType { - case .all: !$0.isCompleted - case .completed: $0.isCompleted - case .flagged: $0.isFlagged - case .list(let list): $0.remindersListID.eq(list.id) - case .scheduled: $0.isScheduled - case .today: $0.isToday - } - } .order { $0.isCompleted } .order { switch ordering { @@ -182,6 +173,17 @@ struct RemindersListDetailView: View { } } .withTags + .where { reminder, _, tag in + switch detailType { + case .all: !reminder.isCompleted + case .completed: reminder.isCompleted + case .flagged: reminder.isFlagged + case .list(let list): reminder.remindersListID.eq(list.id) + case .scheduled: reminder.isScheduled + case .tags(let tags): tag.id.ifnull(0).in(tags.map(\.id)) + case .today: reminder.isToday + } + } .join(RemindersList.all) { $0.remindersListID.eq($3.id) } .select { ReminderState.Columns( @@ -216,6 +218,7 @@ extension RemindersListDetailView.DetailType { case .flagged: "flagged" case .list(let list): "list_\(list.id)" case .scheduled: "scheduled" + case .tags: "tags" case .today: "today" } } @@ -226,6 +229,12 @@ extension RemindersListDetailView.DetailType { case .flagged: "Flagged" case .list(let list): list.title case .scheduled: "Scheduled" + case .tags(let tags): + switch tags.count { + case 0: "Tags" + case 1: "#\(tags[0].title)" + default: "\(tags.count) tags" + } case .today: "Today" } } @@ -236,6 +245,7 @@ extension RemindersListDetailView.DetailType { case .flagged: .orange case .list(let list): list.color case .scheduled: .red + case .tags: .blue case .today: .blue } } @@ -243,14 +253,25 @@ extension RemindersListDetailView.DetailType { struct RemindersListDetailPreview: PreviewProvider { static var previews: some View { - let remindersList = try! prepareDependencies { + let (remindersList, tag) = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() return try $0.defaultDatabase.read { db in - try RemindersList.all.fetchOne(db)! + ( + try RemindersList.all.fetchOne(db)!, + try Tag.all.fetchOne(db)! + ) } } - NavigationStack { - RemindersListDetailView(detailType: .list(remindersList)) + let detailTypes: [RemindersListDetailView.DetailType] = [ + .all, + .list(remindersList), + .tags([tag]), + ] + ForEach(detailTypes, id: \.self) { detailType in + NavigationStack { + RemindersListDetailView(detailType: detailType) + } + .previewDisplayName(detailType.navigationTitle) } } } diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 14eb60ce..dc16e4c6 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -37,6 +37,17 @@ struct RemindersListsView: View { ) private var remindersLists + @SharedReader( + .fetchAll( + Tag + .order(by: \.title) + .withReminders + .having { $2.count().gt(0) }, + animation: .default + ) + ) + private var tags + @SharedReader( .fetchOne( Reminder.select { @@ -141,6 +152,25 @@ struct RemindersListsView: View { .padding([.leading, .trailing], 4) } .listRowInsets(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)) + + Section { + ForEach(tags) { tag in + NavigationLink { + RemindersListDetailView(detailType: .tags([tag])) + } label: { + TagRow(tag: tag) + } + } + + } header: { + Text("Tags") + .font(.system(.title2, design: .rounded, weight: .bold)) + .foregroundStyle(Color(.label)) + .textCase(nil) + .padding(.top, -16) + .padding([.leading, .trailing], 4) + } + .listRowInsets(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)) } else { SearchRemindersView(searchText: searchText) } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index e093658c..a25d5d4d 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -34,6 +34,7 @@ struct Reminder: Equatable, Identifiable { .leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) } .leftJoin(Tag.all) { $1.tagID.eq($2.id) } } + extension Reminder.TableColumns { var isPastDue: some QueryExpression { !isCompleted && #sql("coalesce(date(\(dueDate)) < date('now'), 0)") @@ -56,9 +57,14 @@ enum Priority: Int, QueryBindable { } @Table -struct Tag { +struct Tag: Hashable, Identifiable { var id: Int var title = "" + + static let withReminders = group(by: \.id) + .leftJoin(ReminderTag.all) { $0.id.eq($1.tagID) } + .leftJoin(Reminder.all) { $1.reminderID.eq($2.id) } + .select { tags, _, _ in tags } } extension Tag?.TableColumns { @@ -68,9 +74,10 @@ extension Tag?.TableColumns { } @Table("remindersTags") -struct ReminderTag { - var reminderID: Int - var tagID: Int +struct ReminderTag: Hashable, Identifiable { + var reminderID: Reminder.ID + var tagID: Tag.ID + var id: Self { self } } func appDatabase() throws -> any DatabaseWriter { diff --git a/Examples/Reminders/TagRow.swift b/Examples/Reminders/TagRow.swift new file mode 100644 index 00000000..ae9c7e27 --- /dev/null +++ b/Examples/Reminders/TagRow.swift @@ -0,0 +1,40 @@ +import SharingGRDB +import SwiftUI + +struct TagRow: View { + let tag: Tag + @Dependency(\.defaultDatabase) var database + var body: some View { + HStack { + Image(systemName: "number.circle.fill") + .font(.largeTitle) + .foregroundStyle(.gray) + .background( + Color.white.clipShape(Circle()).padding(4) + ) + Text(tag.title) + Spacer() + } + .swipeActions { + Button { + withErrorReporting { + try database.write { db in + try Tag.delete(tag) + .execute(db) + } + } + } label: { + Image(systemName: "trash") + } + .tint(.red) + } + } +} + +#Preview { + NavigationStack { + List { + TagRow(tag: Tag(id: 1, title: "optional")) + } + } +} diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index ca1ad6db..735cddf7 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -47,13 +47,10 @@ struct TagsView: View { func fetch(_ db: Database) throws -> Value { let top = try Tag - .group(by: \.id) - .join(ReminderTag.all) { $0.id.eq($1.tagID) } - .join(Reminder.all) { $1.reminderID.eq($2.id) } + .withReminders .having { $2.count().gt(0) } .order { ($2.count().desc(), $0.title) } .limit(3) - .select { tags, _, _ in tags } .fetchAll(db) let rest = diff --git a/Examples/SyncUps/SyncUpDetail.swift b/Examples/SyncUps/SyncUpDetail.swift index 329f285a..20ae80d9 100644 --- a/Examples/SyncUps/SyncUpDetail.swift +++ b/Examples/SyncUps/SyncUpDetail.swift @@ -114,7 +114,7 @@ final class SyncUpDetailModel: HashableObject { attendees: Attendee.where { $0.syncUpID == syncUp.id }.fetchAll(db), meetings: Meeting - .where { $0.syncUpID == syncUp.id } + .where { $0.syncUpID.eq(syncUp.id) } .order { $0.date.desc() } .fetchAll(db), syncUp: syncUp From 2d047f712e914c73a18d768216218dd889c0c860 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 19 Apr 2025 12:25:31 -0700 Subject: [PATCH 138/171] clean up --- Examples/Reminders/RemindersListDetail.swift | 69 ++++++++++---------- Examples/Reminders/RemindersLists.swift | 1 - 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 5e4832c7..b750390d 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -157,45 +157,44 @@ struct RemindersListDetailView: View { } fileprivate var remindersKey: some SharedReaderKey<[ReminderState]> { - .fetchAll( + let query = Reminder - .where { - if !showCompleted { - !$0.isCompleted - } + .where { + if !showCompleted { + !$0.isCompleted } - .order { $0.isCompleted } - .order { - switch ordering { - case .dueDate: $0.dueDate - case .priority: ($0.priority.desc(), $0.isFlagged.desc()) - case .title: $0.title - } + } + .order { $0.isCompleted } + .order { + switch ordering { + case .dueDate: $0.dueDate + case .priority: ($0.priority.desc(), $0.isFlagged.desc()) + case .title: $0.title } - .withTags - .where { reminder, _, tag in - switch detailType { - case .all: !reminder.isCompleted - case .completed: reminder.isCompleted - case .flagged: reminder.isFlagged - case .list(let list): reminder.remindersListID.eq(list.id) - case .scheduled: reminder.isScheduled - case .tags(let tags): tag.id.ifnull(0).in(tags.map(\.id)) - case .today: reminder.isToday - } + } + .withTags + .where { reminder, _, tag in + switch detailType { + case .all: !reminder.isCompleted + case .completed: reminder.isCompleted + case .flagged: reminder.isFlagged + case .list(let list): reminder.remindersListID.eq(list.id) + case .scheduled: reminder.isScheduled + case .tags(let tags): tag.id.ifnull(0).in(tags.map(\.id)) + case .today: reminder.isToday } - .join(RemindersList.all) { $0.remindersListID.eq($3.id) } - .select { - ReminderState.Columns( - reminder: $0, - remindersList: $3, - isPastDue: $0.isPastDue, - notes: $0.inlineNotes.substr(0, 200), - tags: $2.jsonNames - ) - }, - animation: .default - ) + } + .join(RemindersList.all) { $0.remindersListID.eq($3.id) } + .select { + ReminderState.Columns( + reminder: $0, + remindersList: $3, + isPastDue: $0.isPastDue, + notes: $0.inlineNotes.substr(0, 200), + tags: $2.jsonNames + ) + } + return .fetchAll(query, animation: .default) } @Selection diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index dc16e4c6..3e392657 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -161,7 +161,6 @@ struct RemindersListsView: View { TagRow(tag: tag) } } - } header: { Text("Tags") .font(.system(.title2, design: .rounded, weight: .bold)) From b570e08a1c655f5641998414b60b003018df78b9 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 19 Apr 2025 12:33:32 -0700 Subject: [PATCH 139/171] clean up --- Examples/Reminders/RemindersLists.swift | 3 ++- Examples/Reminders/Schema.swift | 1 - Examples/Reminders/TagsForm.swift | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 3e392657..a8f3d832 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -42,7 +42,8 @@ struct RemindersListsView: View { Tag .order(by: \.title) .withReminders - .having { $2.count().gt(0) }, + .having { $2.count().gt(0) } + .select { tag, _, _ in tag }, animation: .default ) ) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index a25d5d4d..84a937b0 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -64,7 +64,6 @@ struct Tag: Hashable, Identifiable { static let withReminders = group(by: \.id) .leftJoin(ReminderTag.all) { $0.id.eq($1.tagID) } .leftJoin(Reminder.all) { $1.reminderID.eq($2.id) } - .select { tags, _, _ in tags } } extension Tag?.TableColumns { diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index 735cddf7..6ff0ac6a 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -50,6 +50,7 @@ struct TagsView: View { .withReminders .having { $2.count().gt(0) } .order { ($2.count().desc(), $0.title) } + .select { tag, _, _ in tag } .limit(3) .fetchAll(db) From b08eb3351b5d6e22531a6d2e8b2a6a826d96e5b5 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 19 Apr 2025 13:26:39 -0700 Subject: [PATCH 140/171] clean up --- Examples/Reminders/RemindersLists.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index a8f3d832..1ff30433 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -26,9 +26,10 @@ struct RemindersListsView: View { RemindersList .group(by: \.id) .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) } + .where { $1.isCompleted.ifnull(false) } .select { ReminderListState.Columns( - reminderCount: #sql("count(iif(\($1.isCompleted), NULL, \($1.id)))"), + reminderCount: $1.id.count(), remindersList: $0 ) }, From 8ff011da1a45dfdf67e3551e6e944a66c7e1d0ec Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 19 Apr 2025 13:29:44 -0700 Subject: [PATCH 141/171] clean up --- Examples/SyncUps/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/SyncUps/README.md b/Examples/SyncUps/README.md index c1416c56..192a72cb 100644 --- a/Examples/SyncUps/README.md +++ b/Examples/SyncUps/README.md @@ -1,9 +1,9 @@ # SyncUpsGRDB -A version of [SyncUps][syncups-gh] that persists its model data using SharingGRDB. +A version of [SyncUps][] that persists its model data using SharingGRDB. -SyncUps is a rebuild of Apple's [Scrumdinger][scrumdinger] demo application, but with a focus on +SyncUps is a rebuild of Apple's [Scrumdinger][] demo application, but with a focus on modern, best practices for SwiftUI development. -[syncups-gh]: https://github.com/pointfreeco/syncups -[scrumdinger]: https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger +[SyncUps]: https://github.com/pointfreeco/syncups +[Scrumdinger]: https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger From 05f3c5c39666f3659822abba30a21587d0d308e3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 19 Apr 2025 18:16:30 -0700 Subject: [PATCH 142/171] wip --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bb6e4abf..e03af19c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ A [fast](#Performance), lightweight replacement for SwiftData, powered by SQL. [![CI](https://github.com/pointfreeco/sharing-grdb/workflows/CI/badge.svg)](https://github.com/pointfreeco/sharing-grdb/actions?query=workflow%3ACI) +[![Slack](https://img.shields.io/badge/slack-chat-informational.svg?label=Slack&logo=slack)](https://www.pointfree.co/slack-invite) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fsharing-grdb%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pointfreeco/sharing-grdb) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fsharing-grdb%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/pointfreeco/sharing-grdb) From 9df7bfd0a6b6082815a48a40e23daad77bed35b1 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 19 Apr 2025 22:34:05 -0700 Subject: [PATCH 143/171] wip --- README.md | 37 +++++++++---------- .../xcshareddata/swiftpm/Package.resolved | 4 +- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e03af19c..053c3b84 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,8 @@ class Item { Both of the above examples fetch items from an external data store using Swift data types, and both are automatically observed by SwiftUI so that views are recomputed when the external data changes, -but SharingGRDB is powered directly by SQLite using [Sharing][sharing-gh], -[StructuredQueries][structured-queries-gh], and [GRDB][grdb], and is usable from UIKit, -`@Observable` models, and more. +but SharingGRDB is powered directly by SQLite using [Sharing][], [StructuredQueries][], and +[GRDB][], and is usable from UIKit, `@Observable` models, and more. For more information on SharingGRDB's querying capabilities, see [Fetching model data][fetching-article]. @@ -222,9 +221,8 @@ the [articles][articles] below to learn how to best utilize this library: ## Performance -SharingGRDB leverages high-performance decoding from [StructuredQueries][structured-queries-gh] to -turn fetched data into your Swift domain types, and has a performance profile similar to invoking -SQLite's C APIs directly. +SharingGRDB leverages high-performance decoding from [StructuredQueries][] to turn fetched data into +your Swift domain types, and has a performance profile similar to invoking SQLite's C APIs directly. See the following benchmarks against [Lighter's performance test suite](https://github.com/Lighter-swift/PerformanceTestSuite) for a @@ -252,33 +250,32 @@ SQL queries, including joins and aggregates, and performance, including indices. With some basic knowledge you can apply this library to your database schema in order to query for data and keep your views up-to-date when data in the database changes, and you can use -[StructuredQueries][structured-queries-gh] to build queries, either using its type-safe, -discoverable query building APIs, or using its `#sql` macro for writing safe SQL strings. +[StructuredQueries][] to build queries, either using its type-safe, discoverable +[query building APIs][], or using its `#sql` macro for writing [safe SQL strings][]. -[query-interface]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/queryinterface -[sharing-gh]: https://github.com/pointfreeco/swift-sharing -[structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries -[grdb]: https://github.com/groue/GRDB.swift -[swift-nav-gh]: https://github.com/pointfreeco/swift-navigation -[observe-docs]: https://swiftpackageindex.com/pointfreeco/swift-navigation/main/documentation/swiftnavigation/objectivec/nsobject/observe(_:)-94oxy +[Sharing]: https://github.com/pointfreeco/swift-sharing +[StructuredQueries]: https://github.com/pointfreeco/swift-structured-queries +[GRDB]: https://github.com/groue/GRDB.swift +[query building APIs]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore +[safe SQL strings]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/safesqlstrings ## Demos This repo comes with _lots_ of examples to demonstrate how to solve common and complex problems with Sharing. Check out [this](./Examples) directory to see them all, including: - * [Case Studies](./Examples/CaseStudies): - A number of case studies demonstrating the built-in features of the library. + * [Case Studies](./Examples/CaseStudies): A number of case studies demonstrating the built-in + features of the library. - * [SyncUps](./Examples/SyncUps): We also rebuilt Apple's [Scrumdinger][scrumdinger] demo - application using modern, best practices for SwiftUI development, including using this library - to query and persist state using SQLite. + * [SyncUps](./Examples/SyncUps): We also rebuilt Apple's [Scrumdinger][] demo application using + modern, best practices for SwiftUI development, including using this library to query and + persist state using SQLite. * [Reminders](./Examples/Reminders): A rebuild of Apple's [Reminders][reminders-app-store] app that uses a SQLite database to model the reminders, lists and tags. It features many advanced queries, such as searching, and stats aggregation. -[scrumdinger]: https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger +[Scrumdinger]: https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger [reminders-app-store]: https://apps.apple.com/us/app/reminders/id1108187841 ## Documentation diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3026515b..f10bda5d 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "59d9f2fa68c027a6b1d4eb2df0ed686e16de5eedcc82faf142099882c77be4fc", + "originHash" : "ecdc193386f5f47e30cdf0c285d2d6e00daf3bf425cabb9a85a0276a9340ac72", "pins" : [ { "identity" : "combine-schedulers", @@ -142,7 +142,7 @@ "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { "branch" : "main", - "revision" : "7f488e1c2169b3bbaaa2a88db2e4db86a8709672" + "revision" : "093d4148979330121affccf9ed962901972f35cb" } }, { From dc90f971c84fbd077510ec57539bfb60a03c8a8f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 19 Apr 2025 22:55:17 -0700 Subject: [PATCH 144/171] wip --- README.md | 2 +- Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 053c3b84..2d7827b4 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ taste of how it compares: ``` Orders.fetchAll setup rampup duration - SQLite (Enlighter-generated) 0 0.144 7.183 + SQLite (generated by Enlighter 1.4.10) 0 0.144 7.183 Lighter (1.4.10) 0 0.164 8.059 SharingGRDB (0.2.0) 0 0.172 8.511 GRDB (7.4.1, manual decoding) 0 0.376 18.819 diff --git a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md index d40fd1ef..5fea1c28 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md +++ b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md @@ -158,7 +158,7 @@ taste of how it compares: ``` Orders.fetchAll setup rampup duration - SQLite (Enlighter-generated) 0 0.144 7.183 + SQLite (generated by Enlighter 1.4.10) 0 0.144 7.183 Lighter (1.4.10) 0 0.164 8.059 SharingGRDB (0.2.0) 0 0.172 8.511 GRDB (7.4.1, manual decoding) 0 0.376 18.819 From 70c77a83424272db12b2778871ecb21fcf1de3ad Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 20 Apr 2025 13:03:13 -0700 Subject: [PATCH 145/171] clean up --- Examples/Reminders/RemindersListRow.swift | 6 +-- Examples/Reminders/RemindersLists.swift | 49 +++++++++++------------ Examples/Reminders/Schema.swift | 7 +++- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Examples/Reminders/RemindersListRow.swift b/Examples/Reminders/RemindersListRow.swift index 59f3e829..553879a9 100644 --- a/Examples/Reminders/RemindersListRow.swift +++ b/Examples/Reminders/RemindersListRow.swift @@ -2,7 +2,7 @@ import SharingGRDB import SwiftUI struct RemindersListRow: View { - let reminderCount: Int + let remindersCount: Int let remindersList: RemindersList @State var editList: RemindersList? @@ -19,7 +19,7 @@ struct RemindersListRow: View { ) Text(remindersList.title) Spacer() - Text("\(reminderCount)") + Text("\(remindersCount)") .foregroundStyle(.gray) } .swipeActions { @@ -54,7 +54,7 @@ struct RemindersListRow: View { NavigationStack { List { RemindersListRow( - reminderCount: 10, + remindersCount: 10, remindersList: RemindersList( id: 1, title: "Personal" diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 1ff30433..cdf95c78 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -6,30 +6,14 @@ import StructuredQueries import SwiftUI struct RemindersListsView: View { - @Selection - fileprivate struct ReminderListState: Identifiable { - var id: RemindersList.ID { remindersList.id } - var reminderCount: Int - var remindersList: RemindersList - } - - @Selection - fileprivate struct Stats { - var allCount = 0 - var flaggedCount = 0 - var scheduledCount = 0 - var todayCount = 0 - } - @SharedReader( .fetchAll( RemindersList .group(by: \.id) - .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) } - .where { $1.isCompleted.ifnull(false) } + .leftJoin(Reminder.incomplete) { $0.id.eq($1.remindersListID) } .select { ReminderListState.Columns( - reminderCount: $1.id.count(), + remindersCount: $1.id.count(), remindersList: $0 ) }, @@ -64,6 +48,27 @@ struct RemindersListsView: View { ) private var stats = Stats() + @State private var destination: Destination? + @State private var remindersDetailType: RemindersListDetailView.DetailType? + @State private var searchText = "" + + @Dependency(\.defaultDatabase) private var database + + @Selection + fileprivate struct ReminderListState: Identifiable { + var id: RemindersList.ID { remindersList.id } + var remindersCount: Int + var remindersList: RemindersList + } + + @Selection + fileprivate struct Stats { + var allCount = 0 + var flaggedCount = 0 + var scheduledCount = 0 + var todayCount = 0 + } + enum Destination: Int, Identifiable { case addList case newReminder @@ -71,12 +76,6 @@ struct RemindersListsView: View { var id: Int { rawValue } } - @State private var destination: Destination? - @State private var remindersDetailType: RemindersListDetailView.DetailType? - @State private var searchText = "" - - @Dependency(\.defaultDatabase) private var database - var body: some View { List { if searchText.isEmpty { @@ -140,7 +139,7 @@ struct RemindersListsView: View { RemindersListDetailView(detailType: .list(state.remindersList)) } label: { RemindersListRow( - reminderCount: state.reminderCount, + remindersCount: state.remindersCount, remindersList: state.remindersList ) } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 84a937b0..634a96e9 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -23,11 +23,14 @@ struct Reminder: Equatable, Identifiable { var priority: Priority? var remindersListID: Int var title = "" +} + +extension Reminder { static let incomplete = Self.where { !$0.isCompleted } static func searching(_ text: String) -> Where { Self.where { $0.title.collate(.nocase).contains(text) - || $0.notes.collate(.nocase).contains(text) + || $0.notes.collate(.nocase).contains(text) } } static let withTags = group(by: \.id) @@ -60,7 +63,9 @@ enum Priority: Int, QueryBindable { struct Tag: Hashable, Identifiable { var id: Int var title = "" +} +extension Tag { static let withReminders = group(by: \.id) .leftJoin(ReminderTag.all) { $0.id.eq($1.tagID) } .leftJoin(Reminder.all) { $1.reminderID.eq($2.id) } From 73c90fba48ff897ee434b06c5635fea21d12f7b5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 10:30:29 -0700 Subject: [PATCH 146/171] Bump --- Package.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 914dfa5a..70cbad4c 100644 --- a/Package.swift +++ b/Package.swift @@ -33,7 +33,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "main"), + .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.1.0"), ], targets: [ .target( diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index f10bda5d..7fe9bc54 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ecdc193386f5f47e30cdf0c285d2d6e00daf3bf425cabb9a85a0276a9340ac72", + "originHash" : "c123e85f414c8126fd280c53c6789cf38f3a215cd31fd23a69329229d50e8257", "pins" : [ { "identity" : "combine-schedulers", @@ -141,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "branch" : "main", - "revision" : "093d4148979330121affccf9ed962901972f35cb" + "revision" : "7375bc75c4acaedffee9923e496b93fab18a7bd7", + "version" : "0.1.0" } }, { From 6fba787a018931cc9af1326699a972a0d143318d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 13:32:08 -0700 Subject: [PATCH 147/171] wip --- Examples/CaseStudies/Animations.swift | 1 - Examples/CaseStudies/DynamicQuery.swift | 1 - Examples/CaseStudies/ObservableModelDemo.swift | 1 - Examples/CaseStudies/SwiftUIDemo.swift | 1 - Examples/CaseStudies/TransactionDemo.swift | 1 - Examples/Reminders/Helpers.swift | 2 +- Examples/Reminders/ReminderForm.swift | 2 -- Examples/Reminders/ReminderRow.swift | 1 - Examples/Reminders/RemindersApp.swift | 3 +-- Examples/Reminders/RemindersListDetail.swift | 1 - Examples/Reminders/RemindersListForm.swift | 3 +-- Examples/Reminders/Schema.swift | 1 - Examples/Reminders/SearchReminders.swift | 1 - Examples/SyncUps/App.swift | 1 - Examples/SyncUps/SyncUpForm.swift | 1 - Examples/SyncUps/SyncUpsList.swift | 1 - 16 files changed, 3 insertions(+), 19 deletions(-) diff --git a/Examples/CaseStudies/Animations.swift b/Examples/CaseStudies/Animations.swift index 180c8a2b..f89cedf2 100644 --- a/Examples/CaseStudies/Animations.swift +++ b/Examples/CaseStudies/Animations.swift @@ -1,4 +1,3 @@ -import Dependencies import SharingGRDB import SwiftUI diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index 43eda9ef..9825a61c 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -1,4 +1,3 @@ -import Dependencies import SharingGRDB import SwiftUI diff --git a/Examples/CaseStudies/ObservableModelDemo.swift b/Examples/CaseStudies/ObservableModelDemo.swift index 984df9ff..081a29dd 100644 --- a/Examples/CaseStudies/ObservableModelDemo.swift +++ b/Examples/CaseStudies/ObservableModelDemo.swift @@ -1,4 +1,3 @@ -import Dependencies import SharingGRDB import SwiftUI diff --git a/Examples/CaseStudies/SwiftUIDemo.swift b/Examples/CaseStudies/SwiftUIDemo.swift index c3933424..b351f817 100644 --- a/Examples/CaseStudies/SwiftUIDemo.swift +++ b/Examples/CaseStudies/SwiftUIDemo.swift @@ -1,4 +1,3 @@ -import Dependencies import SharingGRDB import SwiftUI diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index 5da8c54a..32a559c0 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -1,4 +1,3 @@ -import Dependencies import SharingGRDB import SwiftUI diff --git a/Examples/Reminders/Helpers.swift b/Examples/Reminders/Helpers.swift index 9251856c..21310bce 100644 --- a/Examples/Reminders/Helpers.swift +++ b/Examples/Reminders/Helpers.swift @@ -1,4 +1,4 @@ -import StructuredQueriesCore +import SharingGRDB import SwiftUI extension Color { diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index f7e9d874..d89ce5f9 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -1,5 +1,3 @@ -import Dependencies -import GRDB import IssueReporting import SharingGRDB import SwiftUI diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index f118361e..595503a3 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -1,4 +1,3 @@ -import Dependencies import SharingGRDB import SwiftUI diff --git a/Examples/Reminders/RemindersApp.swift b/Examples/Reminders/RemindersApp.swift index c08b5c39..57443a92 100644 --- a/Examples/Reminders/RemindersApp.swift +++ b/Examples/Reminders/RemindersApp.swift @@ -1,5 +1,4 @@ -import Dependencies -import GRDB +import SharingGRDB import SwiftUI @main diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index b750390d..377577df 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -1,5 +1,4 @@ import CasePaths -import Sharing import SharingGRDB import SwiftUI diff --git a/Examples/Reminders/RemindersListForm.swift b/Examples/Reminders/RemindersListForm.swift index aadab767..37bb2acc 100644 --- a/Examples/Reminders/RemindersListForm.swift +++ b/Examples/Reminders/RemindersListForm.swift @@ -1,6 +1,5 @@ -import Dependencies -import GRDB import IssueReporting +import SharingGRDB import SwiftUI struct RemindersListForm: View { diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 634a96e9..13916f9b 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -1,5 +1,4 @@ import Foundation -import GRDB import IssueReporting import SharingGRDB import SwiftUI diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index ac013833..e71082c4 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -1,6 +1,5 @@ import IssueReporting import SharingGRDB -import StructuredQueries import SwiftUI struct SearchRemindersView: View { diff --git a/Examples/SyncUps/App.swift b/Examples/SyncUps/App.swift index 826d63f3..a3b6a0fb 100644 --- a/Examples/SyncUps/App.swift +++ b/Examples/SyncUps/App.swift @@ -1,5 +1,4 @@ import CasePaths -import Dependencies import SharingGRDB import SwiftUI diff --git a/Examples/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUpForm.swift index bd186849..369b61b7 100644 --- a/Examples/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUpForm.swift @@ -1,4 +1,3 @@ -import Dependencies import SharingGRDB import SwiftUI import SwiftUINavigation diff --git a/Examples/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUpsList.swift index 119ae68d..0a2ace36 100644 --- a/Examples/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUpsList.swift @@ -1,5 +1,4 @@ import SharingGRDB -import StructuredQueries import SwiftUI import SwiftUINavigation From d77bd2c73ec963b5a11a2e7f2467ec650b3100b7 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 13:38:32 -0700 Subject: [PATCH 148/171] wip --- Examples/Examples.xcodeproj/project.pbxproj | 2 -- Examples/Reminders/RemindersLists.swift | 7 ++----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index b13fd052..99361140 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -734,7 +734,6 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=50"; PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Reminders; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -762,7 +761,6 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=50"; PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Reminders; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index cdf95c78..f3291322 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -10,12 +10,9 @@ struct RemindersListsView: View { .fetchAll( RemindersList .group(by: \.id) - .leftJoin(Reminder.incomplete) { $0.id.eq($1.remindersListID) } + .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) && !$1.isCompleted } .select { - ReminderListState.Columns( - remindersCount: $1.id.count(), - remindersList: $0 - ) + ReminderListState.Columns(remindersCount: $1.id.count(), remindersList: $0) }, animation: .default ) From aaa4557142777e6be865667fa3aa2e941cf055d0 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 13:39:24 -0700 Subject: [PATCH 149/171] fix --- Examples/Reminders/ReminderForm.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index d89ce5f9..8aaff28c 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -134,7 +134,7 @@ struct ReminderFormView: View { else { return } do { selectedTags = try await database.read { db in - try Tag.select(\.self) + try Tag .order(by: \.title) .join(ReminderTag.all) { $0.id.eq($1.tagID) } .where { $1.reminderID.eq(reminderID) } From 9e44498a65bbd22b4e0f0407d193d84672f21f84 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 14:05:45 -0700 Subject: [PATCH 150/171] wip --- Examples/Reminders/ReminderForm.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 8aaff28c..4175fb6d 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -138,6 +138,7 @@ struct ReminderFormView: View { .order(by: \.title) .join(ReminderTag.all) { $0.id.eq($1.tagID) } .where { $1.reminderID.eq(reminderID) } + .select { tag, _ in tag } .fetchAll(db) } } catch { From 45f0acdd159780d4e6f251630f96bf35b699796e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 15:59:56 -0700 Subject: [PATCH 151/171] wip --- Examples/Reminders/ReminderForm.swift | 2 +- Examples/Reminders/RemindersListDetail.swift | 10 +- Examples/Reminders/RemindersLists.swift | 64 ++- Examples/Reminders/SearchReminders.swift | 12 +- Examples/Reminders/TagsForm.swift | 2 +- Examples/SyncUps/SyncUpDetail.swift | 6 +- Examples/SyncUps/SyncUpsList.swift | 14 +- Sources/SharingGRDBCore/Fetch.swift | 98 ++++ Sources/SharingGRDBCore/FetchAll.swift | 383 ++++++++++++++++ Sources/SharingGRDBCore/FetchKey.swift | 12 +- Sources/SharingGRDBCore/FetchOne.swift | 419 ++++++++++++++++++ .../StructuredQueries/StatementKey.swift | 12 +- 12 files changed, 960 insertions(+), 74 deletions(-) create mode 100644 Sources/SharingGRDBCore/Fetch.swift create mode 100644 Sources/SharingGRDBCore/FetchAll.swift create mode 100644 Sources/SharingGRDBCore/FetchOne.swift diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index d89ce5f9..76752a1f 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -3,7 +3,7 @@ import SharingGRDB import SwiftUI struct ReminderFormView: View { - @SharedReader(.fetchAll(RemindersList.order(by: \.title))) var remindersLists + @FetchAll(RemindersList.order(by: \.title)) var remindersLists @State var isPresentingTagsPopover = false @State var remindersList: RemindersList diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 377577df..e5e12c21 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -3,7 +3,7 @@ import SharingGRDB import SwiftUI struct RemindersListDetailView: View { - @SharedReader(value: []) private var reminderStates: [ReminderState] + @FetchAll private var reminderStates: [ReminderState] @AppStorage private var ordering: Ordering @AppStorage private var showCompleted: Bool @@ -21,7 +21,7 @@ struct RemindersListDetailView: View { wrappedValue: detailType == .completed, "show_completed_list_\(detailType.id)" ) - _reminderStates = SharedReader(wrappedValue: [], remindersKey) + _reminderStates = FetchAll(remindersQuery, animation: .default) } var body: some View { @@ -152,10 +152,10 @@ struct RemindersListDetailView: View { } private func updateQuery() async throws { - try await $reminderStates.load(remindersKey) + try await $reminderStates.load(remindersQuery) } - fileprivate var remindersKey: some SharedReaderKey<[ReminderState]> { + fileprivate var remindersQuery: some StructuredQueriesCore.Statement { let query = Reminder .where { @@ -193,7 +193,7 @@ struct RemindersListDetailView: View { tags: $2.jsonNames ) } - return .fetchAll(query, animation: .default) + return query } @Selection diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index cdf95c78..66b76662 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -1,50 +1,40 @@ -import Dependencies -import GRDB -import Sharing import SharingGRDB -import StructuredQueries import SwiftUI struct RemindersListsView: View { - @SharedReader( - .fetchAll( - RemindersList - .group(by: \.id) - .leftJoin(Reminder.incomplete) { $0.id.eq($1.remindersListID) } - .select { - ReminderListState.Columns( - remindersCount: $1.id.count(), - remindersList: $0 - ) - }, - animation: .default - ) + @FetchAll( + RemindersList + .group(by: \.id) + .leftJoin(Reminder.incomplete) { $0.id.eq($1.remindersListID) } + .select { + ReminderListState.Columns( + remindersCount: $1.count(), + remindersList: $0 + ) + }, + animation: .default ) private var remindersLists - @SharedReader( - .fetchAll( - Tag - .order(by: \.title) - .withReminders - .having { $2.count().gt(0) } - .select { tag, _, _ in tag }, - animation: .default - ) + @FetchAll( + Tag + .order(by: \.title) + .withReminders + .having { $2.count().gt(0) } + .select { tag, _, _ in tag }, + animation: .default ) private var tags - @SharedReader( - .fetchOne( - Reminder.select { - Stats.Columns( - allCount: $0.count(filter: !$0.isCompleted), - flaggedCount: $0.count(filter: $0.isFlagged), - scheduledCount: $0.count(filter: $0.isScheduled), - todayCount: $0.count(filter: $0.isToday) - ) - } - ) + @FetchOne( + Reminder.select { + Stats.Columns( + allCount: $0.count(filter: !$0.isCompleted), + flaggedCount: $0.count(filter: $0.isFlagged), + scheduledCount: $0.count(filter: $0.isScheduled), + todayCount: $0.count(filter: $0.isToday) + ) + } ) private var stats = Stats() diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index e71082c4..c568edfe 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -3,7 +3,7 @@ import SharingGRDB import SwiftUI struct SearchRemindersView: View { - @SharedReader(value: 0) var completedCount: Int + @FetchOne var completedCount: Int = 0 @State.SharedReader(value: []) var reminders: [ReminderState] let searchText: String @@ -62,12 +62,10 @@ struct SearchRemindersView: View { showCompletedInSearchResults = false } try await $completedCount.load( - .fetchOne( - Reminder.searching(searchText) - .where(\.isCompleted) - .count(), - animation: .default - ) + Reminder.searching(searchText) + .where(\.isCompleted) + .count(), + animation: .default ) try await $reminders.load(searchKey) } diff --git a/Examples/Reminders/TagsForm.swift b/Examples/Reminders/TagsForm.swift index 6ff0ac6a..8ba5b1a1 100644 --- a/Examples/Reminders/TagsForm.swift +++ b/Examples/Reminders/TagsForm.swift @@ -2,7 +2,7 @@ import SharingGRDB import SwiftUI struct TagsView: View { - @SharedReader(.fetch(Tags())) var tags = Tags.Value() + @Fetch(Tags()) var tags = Tags.Value() @Binding var selectedTags: [Tag] @Environment(\.dismiss) var dismiss diff --git a/Examples/SyncUps/SyncUpDetail.swift b/Examples/SyncUps/SyncUpDetail.swift index 20ae80d9..4d477e7c 100644 --- a/Examples/SyncUps/SyncUpDetail.swift +++ b/Examples/SyncUps/SyncUpDetail.swift @@ -7,7 +7,7 @@ import SwiftUINavigation final class SyncUpDetailModel: HashableObject { var destination: Destination? var isDismissed = false - @ObservationIgnored @SharedReader var details: Details.Value + @ObservationIgnored @Fetch var details: Details.Value var onMeetingStarted: (SyncUp, [Attendee]) -> Void = unimplemented("onMeetingStarted") @@ -33,9 +33,9 @@ final class SyncUpDetailModel: HashableObject { syncUp: SyncUp ) { self.destination = destination - _details = SharedReader( + _details = Fetch( wrappedValue: Details.Value(syncUp: syncUp), - .fetch(Details(syncUp: syncUp), animation: .default) + Details(syncUp: syncUp), animation: .default ) } diff --git a/Examples/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUpsList.swift index 0a2ace36..4d9ff033 100644 --- a/Examples/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUpsList.swift @@ -7,14 +7,12 @@ import SwiftUINavigation final class SyncUpsListModel { var addSyncUp: SyncUpFormModel? @ObservationIgnored - @SharedReader( - .fetchAll( - SyncUp - .group(by: \.id) - .leftJoin(Attendee.all) { $0.id.eq($1.syncUpID) } - .select { Record.Columns(attendeeCount: $1.count(), syncUp: $0) }, - animation: .default - ) + @FetchAll( + SyncUp + .group(by: \.id) + .leftJoin(Attendee.all) { $0.id.eq($1.syncUpID) } + .select { Record.Columns(attendeeCount: $1.count(), syncUp: $0) }, + animation: .default ) var syncUps: [Record] @ObservationIgnored @Dependency(\.uuid) var uuid diff --git a/Sources/SharingGRDBCore/Fetch.swift b/Sources/SharingGRDBCore/Fetch.swift new file mode 100644 index 00000000..7a92103a --- /dev/null +++ b/Sources/SharingGRDBCore/Fetch.swift @@ -0,0 +1,98 @@ +#if canImport(Combine) +import Combine +#endif +#if canImport(SwiftUI) + import SwiftUI +#endif + +@propertyWrapper +public struct Fetch: Sendable { + public var _sharedReader: SharedReader + + public var wrappedValue: Value { + _sharedReader.wrappedValue + } + + public var projectedValue: Self { + self + } + + public var loadError: (any Error)? { + _sharedReader.loadError + } + + public var isLoading: Bool { + _sharedReader.isLoading + } + + #if canImport(Combine) + public var publisher: some Publisher { + _sharedReader.publisher + } + #endif + + public init(wrappedValue: sending Value) { + _sharedReader = SharedReader(value: wrappedValue) + } + + public init( + wrappedValue: Value, + _ request: some FetchKeyRequest, + database: (any DatabaseReader)? = nil + ) { + _sharedReader = SharedReader(wrappedValue: wrappedValue, .fetch(request, database: database)) + } + + public init( + _ request: some FetchKeyRequest, + database: (any DatabaseReader)? = nil + ) where Value: RangeReplaceableCollection { + _sharedReader = SharedReader(.fetch(request, database: database)) + } +} + +extension Fetch { + public init( + wrappedValue: Value, + _ request: some FetchKeyRequest, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, + .fetch(request, database: database, scheduler: scheduler) + ) + } + + public init( + _ request: some FetchKeyRequest, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) where Value: RangeReplaceableCollection { + _sharedReader = SharedReader(.fetch(request, database: database, scheduler: scheduler)) + } +} + +#if canImport(SwiftUI) + extension Fetch { + public init( + wrappedValue: Value, + _ request: some FetchKeyRequest, + database: (any DatabaseReader)? = nil, + animation: Animation + ) { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, + .fetch(request, database: database, animation: animation) + ) + } + + public init( + _ request: some FetchKeyRequest, + database: (any DatabaseReader)? = nil, + animation: Animation + ) where Value: RangeReplaceableCollection { + _sharedReader = SharedReader(.fetch(request, database: database, animation: animation)) + } + } +#endif diff --git a/Sources/SharingGRDBCore/FetchAll.swift b/Sources/SharingGRDBCore/FetchAll.swift new file mode 100644 index 00000000..dd9976bd --- /dev/null +++ b/Sources/SharingGRDBCore/FetchAll.swift @@ -0,0 +1,383 @@ +#if canImport(Combine) +import Combine +#endif +#if canImport(SwiftUI) + import SwiftUI +#endif + +@propertyWrapper +public struct FetchAll: Sendable { + public var _sharedReader: SharedReader<[Element]> = SharedReader(value: []) + + public var wrappedValue: [Element] { + _sharedReader.wrappedValue + } + + public var projectedValue: Self { + self + } + + public var loadError: (any Error)? { + _sharedReader.loadError + } + + public var isLoading: Bool { + _sharedReader.isLoading + } + + #if canImport(Combine) + public var publisher: some Publisher<[Element], Never> { + _sharedReader.publisher + } + #endif + + public init() { + _sharedReader = SharedReader(value: []) + } + + public init( + _ statement: S, + database: (any DatabaseReader)? = nil + ) + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + _sharedReader = SharedReader(.fetchAll(statement, database: database)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + _ statement: S, + database: (any DatabaseReader)? = nil + ) + where + Element == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + _sharedReader = SharedReader(.fetchAll(statement, database: database)) + } + + public init( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil + ) + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + _sharedReader = SharedReader(.fetchAll(statement, database: database)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil + ) + where + Element == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + _sharedReader = SharedReader(.fetchAll(statement, database: database)) + } + + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil + ) async throws + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + try await _sharedReader.load(.fetchAll(statement, database: database)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil + ) async throws + where + Element == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database)) + } + + public func load( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil + ) async throws + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil + ) async throws + where + Element == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database)) + } +} + +extension FetchAll { + public init( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + _sharedReader = SharedReader(.fetchAll(statement, database: database, scheduler: scheduler)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) + where + Element == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + _sharedReader = SharedReader(.fetchAll(statement, database: database, scheduler: scheduler)) + } + + public init( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + _sharedReader = SharedReader(.fetchAll(statement, database: database, scheduler: scheduler)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) + where + Element == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + _sharedReader = SharedReader(.fetchAll(statement, database: database, scheduler: scheduler)) + } + + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) async throws + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + try await _sharedReader.load(.fetchAll(statement, database: database, scheduler: scheduler)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) async throws + where + Element == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database, scheduler: scheduler)) + } + + public func load( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) async throws + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database, scheduler: scheduler)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) async throws + where + Element == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database, scheduler: scheduler)) + } +} + +#if canImport(SwiftUI) + extension FetchAll { + public init( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + _sharedReader = SharedReader(.fetchAll(statement, database: database, animation: animation)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Element == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + _sharedReader = SharedReader(.fetchAll(statement, database: database, animation: animation)) + } + + public init( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + _sharedReader = SharedReader(.fetchAll(statement, database: database, animation: animation)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Element == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + _sharedReader = SharedReader(.fetchAll(statement, database: database, animation: animation)) + } + + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + try await _sharedReader.load(.fetchAll(statement, database: database, animation: animation)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Element == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database, animation: animation)) + } + + public func load( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database, animation: animation)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Element == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database, animation: animation)) + } + } +#endif diff --git a/Sources/SharingGRDBCore/FetchKey.swift b/Sources/SharingGRDBCore/FetchKey.swift index 66c90a69..1dd64c0e 100644 --- a/Sources/SharingGRDBCore/FetchKey.swift +++ b/Sources/SharingGRDBCore/FetchKey.swift @@ -100,7 +100,7 @@ extension SharedReaderKey { ) -> Self where Self == FetchKey<[Record]>.Default { Self[ - .fetch(FetchAll(sql: sql, arguments: arguments), database: database), + .fetch(FetchAllRequest(sql: sql, arguments: arguments), database: database), default: [] ] } @@ -128,7 +128,7 @@ extension SharedReaderKey { database: (any DatabaseReader)? = nil ) -> Self where Self == FetchKey { - .fetch(FetchOne(sql: sql, arguments: arguments), database: database) + .fetch(FetchOneRequest(sql: sql, arguments: arguments), database: database) } } @@ -199,7 +199,7 @@ extension SharedReaderKey { ) -> Self where Self == FetchKey<[Record]>.Default { Self[ - .fetch(FetchAll(sql: sql, arguments: arguments), database: database, scheduler: scheduler), + .fetch(FetchAllRequest(sql: sql, arguments: arguments), database: database, scheduler: scheduler), default: [] ] } @@ -225,7 +225,7 @@ extension SharedReaderKey { scheduler: some ValueObservationScheduler & Hashable ) -> Self where Self == FetchKey { - .fetch(FetchOne(sql: sql, arguments: arguments), database: database, scheduler: scheduler) + .fetch(FetchOneRequest(sql: sql, arguments: arguments), database: database, scheduler: scheduler) } } @@ -377,7 +377,7 @@ public struct FetchKeyID: Hashable { } } -private struct FetchAll: FetchKeyRequest { +private struct FetchAllRequest: FetchKeyRequest { var sql: String var arguments: StatementArguments = StatementArguments() func fetch(_ db: Database) throws -> [Element] { @@ -385,7 +385,7 @@ private struct FetchAll: FetchKeyRequest { } } -private struct FetchOne: FetchKeyRequest { +private struct FetchOneRequest: FetchKeyRequest { var sql: String var arguments: StatementArguments = StatementArguments() func fetch(_ db: Database) throws -> Value { diff --git a/Sources/SharingGRDBCore/FetchOne.swift b/Sources/SharingGRDBCore/FetchOne.swift new file mode 100644 index 00000000..e7886153 --- /dev/null +++ b/Sources/SharingGRDBCore/FetchOne.swift @@ -0,0 +1,419 @@ +#if canImport(Combine) +import Combine +#endif +#if canImport(SwiftUI) + import SwiftUI +#endif + +@propertyWrapper +public struct FetchOne: Sendable { + public var _sharedReader: SharedReader + + public var wrappedValue: Value { + _sharedReader.wrappedValue + } + + public var projectedValue: Self { + self + } + + public var loadError: (any Error)? { + _sharedReader.loadError + } + + public var isLoading: Bool { + _sharedReader.isLoading + } + + #if canImport(Combine) + public var publisher: some Publisher { + _sharedReader.publisher + } + #endif + + public init(wrappedValue: sending Value) { + _sharedReader = SharedReader(value: wrappedValue) + } + + public init( + wrappedValue: S.From.QueryOutput, + _ statement: S, + database: (any DatabaseReader)? = nil + ) + where + Value == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database) + ) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: (S.From.QueryOutput, repeat (each J).QueryOutput), + _ statement: S, + database: (any DatabaseReader)? = nil + ) + where + Value == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database) + ) + } + + public init( + wrappedValue: V.QueryOutput, + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil + ) + where + Value == V.QueryOutput, + V.QueryOutput: Sendable + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database) + ) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: (V1.QueryOutput, repeat (each V2).QueryOutput), + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil + ) + where + Value == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database) + ) + } + + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil + ) async throws + where + Value == [S.From.QueryOutput], + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + try await _sharedReader.load(.fetchAll(statement, database: database)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil + ) async throws + where + Value == [(S.From.QueryOutput, repeat (each J).QueryOutput)], + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database)) + } + + public func load( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil + ) async throws + where + Value == [V.QueryOutput], + V.QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil + ) async throws + where + Value == [(V1.QueryOutput, repeat (each V2).QueryOutput)], + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchAll(statement, database: database)) + } +} + +extension FetchOne { + public init( + wrappedValue: S.From.QueryOutput, + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) + where + Value == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database, scheduler: scheduler) + ) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: (S.From.QueryOutput, repeat (each J).QueryOutput), + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) + where + Value == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database, scheduler: scheduler) + ) + } + + public init( + wrappedValue: V.QueryOutput, + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) + where + Value == V.QueryOutput, + V.QueryOutput: Sendable + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database, scheduler: scheduler) + ) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: (V1.QueryOutput, repeat (each V2).QueryOutput), + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) + where + Value == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database, scheduler: scheduler) + ) + } + + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) async throws + where + Value == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + try await _sharedReader.load(.fetchOne(statement, database: database, scheduler: scheduler)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) async throws + where + Value == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchOne(statement, database: database, scheduler: scheduler)) + } + + public func load( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) async throws + where + Value == V.QueryOutput, + V.QueryOutput: Sendable + { + try await _sharedReader.load(.fetchOne(statement, database: database, scheduler: scheduler)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) async throws + where + Value == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchOne(statement, database: database, scheduler: scheduler)) + } +} + +#if canImport(SwiftUI) +extension FetchOne { + public init( + wrappedValue: S.From.QueryOutput, + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Value == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database, animation: animation) + ) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: (S.From.QueryOutput, repeat (each J).QueryOutput), + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Value == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database, animation: animation) + ) + } + + public init( + wrappedValue: V.QueryOutput, + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Value == V.QueryOutput, + V.QueryOutput: Sendable + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database, animation: animation) + ) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: (V1.QueryOutput, repeat (each V2).QueryOutput), + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Value == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + _sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetchOne(statement, database: database, animation: animation) + ) + } + + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Value == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + try await _sharedReader.load(.fetchOne(statement, database: database, animation: animation)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Value == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchOne(statement, database: database, animation: animation)) + } + + public func load( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Value == V.QueryOutput, + V.QueryOutput: Sendable + { + try await _sharedReader.load(.fetchOne(statement, database: database, animation: animation)) + } + + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Value ==(V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + try await _sharedReader.load(.fetchOne(statement, database: database, animation: animation)) + } +} +#endif diff --git a/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift b/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift index ea83bcc8..1a5cf635 100644 --- a/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift +++ b/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift @@ -30,9 +30,9 @@ extension SharedReaderKey { database: (any DatabaseReader)? = nil ) -> Self where - S.QueryValue == (), - S.Joins == (), - Self == FetchKey<[S.From.QueryOutput]>.Default + S.QueryValue == (), + S.Joins == (), + Self == FetchKey<[S.From.QueryOutput]>.Default { let statement = statement.selectStar() return fetchAll(statement, database: database) @@ -73,9 +73,9 @@ extension SharedReaderKey { database: (any DatabaseReader)? = nil ) -> Self where - S.QueryValue == (), - S.Joins == (), - Self == FetchKey + S.QueryValue == (), + S.Joins == (), + Self == FetchKey { let statement = statement.selectStar() return fetchOne(statement, database: database) From 655599142a170318400852d2b002e1ad0fb9c82a Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 16:00:11 -0700 Subject: [PATCH 152/171] wip --- Examples/Examples.xcodeproj/project.pbxproj | 21 ------------------- Examples/Reminders/RemindersListDetail.swift | 2 +- Examples/Reminders/Schema.swift | 4 ++-- Examples/Reminders/SearchReminders.swift | 2 +- Package.swift | 3 ++- .../xcshareddata/swiftpm/Package.resolved | 11 +--------- 6 files changed, 7 insertions(+), 36 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 99361140..d3c6a173 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -10,11 +10,8 @@ CA14DBC92DA884C400E36852 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = CA14DBC82DA884C400E36852 /* CasePaths */; }; CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA2908C82D4AF70E003F165F /* UIKitNavigation */; }; CAD001872D874F1F00FA977A /* DependenciesTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = CAD001862D874F1F00FA977A /* DependenciesTestSupport */; }; - CAE6C64D2D69017D00CE1C90 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */; }; - CAF3EAB92D84D85400E7E0D0 /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAF3EAB82D84D85400E7E0D0 /* StructuredQueriesGRDB */; }; CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CAFDD6492D5E823A00EE099E /* SharingGRDB */; }; DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = DC5FA7472D4C63D60082743E /* DependenciesMacros */; }; - DC876AFA2D9609660022207D /* StructuredQueriesGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DC876AF92D9609660022207D /* StructuredQueriesGRDB */; }; DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A132D4842BF0071F499 /* CasePaths */; }; DCBE8A162D4842C80071F499 /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A152D4842C80071F499 /* SharingGRDB */; }; DCF267392D48437300B680BE /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = DCF267382D48437300B680BE /* SwiftUINavigation */; }; @@ -122,7 +119,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DC876AFA2D9609660022207D /* StructuredQueriesGRDB in Frameworks */, DCF2684A2D4993BC00B680BE /* SharingGRDB in Frameworks */, CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */, ); @@ -140,7 +136,6 @@ buildActionMask = 2147483647; files = ( CAFDD64A2D5E823A00EE099E /* SharingGRDB in Frameworks */, - CAE6C64D2D69017D00CE1C90 /* StructuredQueriesGRDB in Frameworks */, CA14DBC92DA884C400E36852 /* CasePaths in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -149,7 +144,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CAF3EAB92D84D85400E7E0D0 /* StructuredQueriesGRDB in Frameworks */, DCF267392D48437300B680BE /* SwiftUINavigation in Frameworks */, DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */, DCBE8A162D4842C80071F499 /* SharingGRDB in Frameworks */, @@ -239,7 +233,6 @@ packageProductDependencies = ( DCF268492D4993BC00B680BE /* SharingGRDB */, CA2908C82D4AF70E003F165F /* UIKitNavigation */, - DC876AF92D9609660022207D /* StructuredQueriesGRDB */, ); productName = Examples; productReference = CAF836982D4735620047AEB5 /* CaseStudies.app */; @@ -286,7 +279,6 @@ name = Reminders; packageProductDependencies = ( CAFDD6492D5E823A00EE099E /* SharingGRDB */, - CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */, CA14DBC82DA884C400E36852 /* CasePaths */, ); productName = Reminders; @@ -314,7 +306,6 @@ DCBE8A152D4842C80071F499 /* SharingGRDB */, DCF267382D48437300B680BE /* SwiftUINavigation */, DC5FA7472D4C63D60082743E /* DependenciesMacros */, - CAF3EAB82D84D85400E7E0D0 /* StructuredQueriesGRDB */, ); productName = SyncUps; productReference = DCBE89CC2D483FB90071F499 /* SyncUps.app */; @@ -926,14 +917,6 @@ package = DC5FA7462D4C63D60082743E /* XCRemoteSwiftPackageReference "swift-dependencies" */; productName = DependenciesTestSupport; }; - CAE6C64C2D69017D00CE1C90 /* StructuredQueriesGRDB */ = { - isa = XCSwiftPackageProductDependency; - productName = StructuredQueriesGRDB; - }; - CAF3EAB82D84D85400E7E0D0 /* StructuredQueriesGRDB */ = { - isa = XCSwiftPackageProductDependency; - productName = StructuredQueriesGRDB; - }; CAFDD6492D5E823A00EE099E /* SharingGRDB */ = { isa = XCSwiftPackageProductDependency; productName = SharingGRDB; @@ -943,10 +926,6 @@ package = DC5FA7462D4C63D60082743E /* XCRemoteSwiftPackageReference "swift-dependencies" */; productName = DependenciesMacros; }; - DC876AF92D9609660022207D /* StructuredQueriesGRDB */ = { - isa = XCSwiftPackageProductDependency; - productName = StructuredQueriesGRDB; - }; DCBE8A132D4842BF0071F499 /* CasePaths */ = { isa = XCSwiftPackageProductDependency; package = DCBE8A122D4842BF0071F499 /* XCRemoteSwiftPackageReference "swift-case-paths" */; diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersListDetail.swift index 377577df..5b2e5837 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersListDetail.swift @@ -190,7 +190,7 @@ struct RemindersListDetailView: View { remindersList: $3, isPastDue: $0.isPastDue, notes: $0.inlineNotes.substr(0, 200), - tags: $2.jsonNames + tags: #sql("\($2.jsonNames)") ) } return .fetchAll(query, animation: .default) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 13916f9b..88ac4bac 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -70,9 +70,9 @@ extension Tag { .leftJoin(Reminder.all) { $1.reminderID.eq($2.id) } } -extension Tag?.TableColumns { +extension Tag.TableColumns { var jsonNames: some QueryExpression> { - #sql("\(self.title)").jsonGroupArray(filter: self.title.isNot(nil)) + self.title.jsonGroupArray(filter: self.title.isNot(nil)) } } diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index e71082c4..33248521 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -86,7 +86,7 @@ struct SearchRemindersView: View { notes: $0.inlineNotes, reminder: $0, remindersList: $3, - tags: $2.jsonNames + tags: #sql("\($2.jsonNames)") ) } return .fetchAll(query, animation: .default) diff --git a/Package.swift b/Package.swift index 70cbad4c..f191c41c 100644 --- a/Package.swift +++ b/Package.swift @@ -33,7 +33,8 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.1.0"), + //.package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.1.0"), + .package(path: "../swift-structured-queries") ], targets: [ .target( diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7fe9bc54..08ef77e8 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c123e85f414c8126fd280c53c6789cf38f3a215cd31fd23a69329229d50e8257", + "originHash" : "9e6f166164ed5ad66ac6a0d8c019ce10dd426bda22ac94e3ff771237609176ea", "pins" : [ { "identity" : "combine-schedulers", @@ -136,15 +136,6 @@ "version" : "1.18.3" } }, - { - "identity" : "swift-structured-queries", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-structured-queries", - "state" : { - "revision" : "7375bc75c4acaedffee9923e496b93fab18a7bd7", - "version" : "0.1.0" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", From 3abceafdeed5ba0abdb9543feca9dfd1bde48442 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 16:39:40 -0700 Subject: [PATCH 153/171] wip --- Examples/CaseStudies/Animations.swift | 2 +- .../CaseStudies/ObservableModelDemo.swift | 4 +- .../CaseStudies/SwiftDataTemplateDemo.swift | 2 +- Examples/CaseStudies/SwiftUIDemo.swift | 4 +- Examples/Reminders/SearchReminders.swift | 40 +- Sources/SharingGRDBCore/FetchAll.swift | 207 +++++- Sources/SharingGRDBCore/FetchOne.swift | 211 +++++- .../Internal/StatementKey.swift | 23 + .../StructuredQueries/StatementKey.swift | 698 ------------------ 9 files changed, 414 insertions(+), 777 deletions(-) create mode 100644 Sources/SharingGRDBCore/Internal/StatementKey.swift delete mode 100644 Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift diff --git a/Examples/CaseStudies/Animations.swift b/Examples/CaseStudies/Animations.swift index f89cedf2..d5380c34 100644 --- a/Examples/CaseStudies/Animations.swift +++ b/Examples/CaseStudies/Animations.swift @@ -12,7 +12,7 @@ struct AnimationsCaseStudy: SwiftUICaseStudy { """ let caseStudyTitle = "Animations" - @SharedReader(.fetchAll(Fact.order { $0.id.desc() }, animation: .default)) + @FetchAll(Fact.order { $0.id.desc() }, animation: .default) private var facts @Dependency(\.defaultDatabase) var database diff --git a/Examples/CaseStudies/ObservableModelDemo.swift b/Examples/CaseStudies/ObservableModelDemo.swift index 081a29dd..a777fb64 100644 --- a/Examples/CaseStudies/ObservableModelDemo.swift +++ b/Examples/CaseStudies/ObservableModelDemo.swift @@ -46,10 +46,10 @@ struct ObservableModelDemo: SwiftUICaseStudy { @MainActor private class Model { @ObservationIgnored - @SharedReader(.fetchAll(Fact.order { $0.id.desc() }, animation: .default)) + @FetchAll(Fact.order { $0.id.desc() }, animation: .default) var facts @ObservationIgnored - @SharedReader(.fetchOne(Fact.count(), animation: .default)) + @FetchOne(Fact.count(), animation: .default) var factsCount = 0 var number = 0 diff --git a/Examples/CaseStudies/SwiftDataTemplateDemo.swift b/Examples/CaseStudies/SwiftDataTemplateDemo.swift index 2d120749..a1e86802 100644 --- a/Examples/CaseStudies/SwiftDataTemplateDemo.swift +++ b/Examples/CaseStudies/SwiftDataTemplateDemo.swift @@ -9,7 +9,7 @@ struct SwiftDataTemplateView: SwiftUICaseStudy { let caseStudyTitle = "SwiftData Template" @Dependency(\.defaultDatabase) private var database - @SharedReader(.fetchAll(Item.all, animation: .default)) private var items + @FetchAll(Item.all, animation: .default) private var items var body: some View { NavigationStack { diff --git a/Examples/CaseStudies/SwiftUIDemo.swift b/Examples/CaseStudies/SwiftUIDemo.swift index b351f817..d4a12335 100644 --- a/Examples/CaseStudies/SwiftUIDemo.swift +++ b/Examples/CaseStudies/SwiftUIDemo.swift @@ -11,9 +11,9 @@ struct SwiftUIDemo: SwiftUICaseStudy { """ let caseStudyTitle = "SwiftUI Views" - @SharedReader(.fetchAll(Fact.order { $0.id.desc() }, animation: .default)) + @FetchAll(Fact.order { $0.id.desc() }, animation: .default) private var facts - @SharedReader(.fetchOne(Fact.count(), animation: .default)) + @FetchOne(Fact.count(), animation: .default) var factsCount = 0 @Dependency(\.defaultDatabase) var database diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index c568edfe..980cbdd6 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -4,7 +4,7 @@ import SwiftUI struct SearchRemindersView: View { @FetchOne var completedCount: Int = 0 - @State.SharedReader(value: []) var reminders: [ReminderState] + @State @FetchAll var reminders: [ReminderState] let searchText: String @State var showCompletedInSearchResults = false @@ -13,6 +13,7 @@ struct SearchRemindersView: View { init(searchText: String) { self.searchText = searchText + _reminders = State(wrappedValue: FetchAll()) } var body: some View { @@ -67,27 +68,24 @@ struct SearchRemindersView: View { .count(), animation: .default ) - try await $reminders.load(searchKey) - } - - private var searchKey: some SharedReaderKey<[ReminderState]> { - let query = + try await $reminders.wrappedValue.load( Reminder - .searching(searchText) - .where { showCompletedInSearchResults || !$0.isCompleted } - .order { ($0.isCompleted, $0.dueDate) } - .withTags - .join(RemindersList.all) { $0.remindersListID.eq($3.id) } - .select { - ReminderState.Columns( - isPastDue: $0.isPastDue, - notes: $0.inlineNotes, - reminder: $0, - remindersList: $3, - tags: $2.jsonNames - ) - } - return .fetchAll(query, animation: .default) + .searching(searchText) + .where { showCompletedInSearchResults || !$0.isCompleted } + .order { ($0.isCompleted, $0.dueDate) } + .withTags + .join(RemindersList.all) { $0.remindersListID.eq($3.id) } + .select { + ReminderState.Columns( + isPastDue: $0.isPastDue, + notes: $0.inlineNotes, + reminder: $0, + remindersList: $3, + tags: $2.jsonNames + ) + }, + animation: .default + ) } private func deleteCompletedReminders(monthsAgo: Int? = nil) { diff --git a/Sources/SharingGRDBCore/FetchAll.swift b/Sources/SharingGRDBCore/FetchAll.swift index dd9976bd..4a6ea770 100644 --- a/Sources/SharingGRDBCore/FetchAll.swift +++ b/Sources/SharingGRDBCore/FetchAll.swift @@ -45,7 +45,13 @@ public struct FetchAll: Sendable { S.From.QueryOutput: Sendable, S.Joins == () { - _sharedReader = SharedReader(.fetchAll(statement, database: database)) + let statement = statement.selectStar().asSelect() + _sharedReader = SharedReader( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database + ) + ) } @_disfavoredOverload @@ -61,7 +67,10 @@ public struct FetchAll: Sendable { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { - _sharedReader = SharedReader(.fetchAll(statement, database: database)) + let statement = statement.selectStar().asSelect() + _sharedReader = SharedReader( + .fetch(FetchAllStatementPackRequest(statement: statement), database: database) + ) } public init( @@ -72,7 +81,12 @@ public struct FetchAll: Sendable { Element == V.QueryOutput, V.QueryOutput: Sendable { - _sharedReader = SharedReader(.fetchAll(statement, database: database)) + _sharedReader = SharedReader( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database + ) + ) } @_disfavoredOverload @@ -86,7 +100,12 @@ public struct FetchAll: Sendable { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - _sharedReader = SharedReader(.fetchAll(statement, database: database)) + _sharedReader = SharedReader( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database + ) + ) } public func load( @@ -99,7 +118,13 @@ public struct FetchAll: Sendable { S.From.QueryOutput: Sendable, S.Joins == () { - try await _sharedReader.load(.fetchAll(statement, database: database)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database + ) + ) } @_disfavoredOverload @@ -115,7 +140,13 @@ public struct FetchAll: Sendable { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database + ) + ) } public func load( @@ -126,7 +157,12 @@ public struct FetchAll: Sendable { Element == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database)) + try await _sharedReader.load( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database + ) + ) } @_disfavoredOverload @@ -140,7 +176,12 @@ public struct FetchAll: Sendable { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database)) + try await _sharedReader.load( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database + ) + ) } } @@ -156,7 +197,14 @@ extension FetchAll { S.From.QueryOutput: Sendable, S.Joins == () { - _sharedReader = SharedReader(.fetchAll(statement, database: database, scheduler: scheduler)) + let statement = statement.selectStar().asSelect() + _sharedReader = SharedReader( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } @_disfavoredOverload @@ -173,7 +221,14 @@ extension FetchAll { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { - _sharedReader = SharedReader(.fetchAll(statement, database: database, scheduler: scheduler)) + let statement = statement.selectStar().asSelect() + _sharedReader = SharedReader( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } public init( @@ -185,7 +240,13 @@ extension FetchAll { Element == V.QueryOutput, V.QueryOutput: Sendable { - _sharedReader = SharedReader(.fetchAll(statement, database: database, scheduler: scheduler)) + _sharedReader = SharedReader( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } @_disfavoredOverload @@ -200,7 +261,13 @@ extension FetchAll { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - _sharedReader = SharedReader(.fetchAll(statement, database: database, scheduler: scheduler)) + _sharedReader = SharedReader( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } public func load( @@ -214,7 +281,14 @@ extension FetchAll { S.From.QueryOutput: Sendable, S.Joins == () { - try await _sharedReader.load(.fetchAll(statement, database: database, scheduler: scheduler)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } @_disfavoredOverload @@ -231,7 +305,14 @@ extension FetchAll { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database, scheduler: scheduler)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } public func load( @@ -243,7 +324,13 @@ extension FetchAll { Element == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database, scheduler: scheduler)) + try await _sharedReader.load( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } @_disfavoredOverload @@ -258,7 +345,12 @@ extension FetchAll { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database, scheduler: scheduler)) + try await _sharedReader.load( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database + ) + ) } } @@ -275,7 +367,14 @@ extension FetchAll { S.From.QueryOutput: Sendable, S.Joins == () { - _sharedReader = SharedReader(.fetchAll(statement, database: database, animation: animation)) + let statement = statement.selectStar().asSelect() + _sharedReader = SharedReader( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + animation: animation + ) + ) } @_disfavoredOverload @@ -292,7 +391,14 @@ extension FetchAll { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { - _sharedReader = SharedReader(.fetchAll(statement, database: database, animation: animation)) + let statement = statement.selectStar().asSelect() + _sharedReader = SharedReader( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database, + animation: animation + ) + ) } public init( @@ -304,7 +410,13 @@ extension FetchAll { Element == V.QueryOutput, V.QueryOutput: Sendable { - _sharedReader = SharedReader(.fetchAll(statement, database: database, animation: animation)) + _sharedReader = SharedReader( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + animation: animation + ) + ) } @_disfavoredOverload @@ -319,7 +431,13 @@ extension FetchAll { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - _sharedReader = SharedReader(.fetchAll(statement, database: database, animation: animation)) + _sharedReader = SharedReader( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database, + animation: animation + ) + ) } public func load( @@ -333,7 +451,14 @@ extension FetchAll { S.From.QueryOutput: Sendable, S.Joins == () { - try await _sharedReader.load(.fetchAll(statement, database: database, animation: animation)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + animation: animation + ) + ) } @_disfavoredOverload @@ -350,7 +475,14 @@ extension FetchAll { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database, animation: animation)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database, + animation: animation + ) + ) } public func load( @@ -362,7 +494,13 @@ extension FetchAll { Element == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database, animation: animation)) + try await _sharedReader.load( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + animation: animation + ) + ) } @_disfavoredOverload @@ -377,7 +515,28 @@ extension FetchAll { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database, animation: animation)) + try await _sharedReader.load( + .fetch( + FetchAllStatementPackRequest(statement: statement), + database: database, + animation: animation + ) + ) } } #endif + +private struct FetchAllStatementValueRequest: StatementKeyRequest { + let statement: any StructuredQueriesCore.Statement + func fetch(_ db: Database) throws -> [Value.QueryOutput] { + try statement.fetchAll(db) + } +} + +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) +private struct FetchAllStatementPackRequest: StatementKeyRequest { + let statement: any StructuredQueriesCore.Statement<(repeat each Value)> + func fetch(_ db: Database) throws -> [(repeat (each Value).QueryOutput)] { + try statement.fetchAll(db) + } +} diff --git a/Sources/SharingGRDBCore/FetchOne.swift b/Sources/SharingGRDBCore/FetchOne.swift index e7886153..4bae7724 100644 --- a/Sources/SharingGRDBCore/FetchOne.swift +++ b/Sources/SharingGRDBCore/FetchOne.swift @@ -46,8 +46,13 @@ public struct FetchOne: Sendable { S.From.QueryOutput: Sendable, S.Joins == () { + let statement = statement.selectStar().asSelect() _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database + ) ) } @@ -65,8 +70,13 @@ public struct FetchOne: Sendable { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { + let statement = statement.selectStar().asSelect() _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database + ) ) } @@ -80,7 +90,11 @@ public struct FetchOne: Sendable { V.QueryOutput: Sendable { _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database + ) ) } @@ -97,7 +111,11 @@ public struct FetchOne: Sendable { repeat (each V2).QueryOutput: Sendable { _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database + ) ) } @@ -106,12 +124,18 @@ public struct FetchOne: Sendable { database: (any DatabaseReader)? = nil ) async throws where - Value == [S.From.QueryOutput], + Value == S.From.QueryOutput, S.QueryValue == (), S.From.QueryOutput: Sendable, S.Joins == () { - try await _sharedReader.load(.fetchAll(statement, database: database)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database + ) + ) } @_disfavoredOverload @@ -121,13 +145,19 @@ public struct FetchOne: Sendable { database: (any DatabaseReader)? = nil ) async throws where - Value == [(S.From.QueryOutput, repeat (each J).QueryOutput)], + Value == (S.From.QueryOutput, repeat (each J).QueryOutput), S.QueryValue == (), S.From.QueryOutput: Sendable, S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database + ) + ) } public func load( @@ -135,10 +165,15 @@ public struct FetchOne: Sendable { database: (any DatabaseReader)? = nil ) async throws where - Value == [V.QueryOutput], + Value == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database)) + try await _sharedReader.load( + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database + ) + ) } @_disfavoredOverload @@ -148,11 +183,16 @@ public struct FetchOne: Sendable { database: (any DatabaseReader)? = nil ) async throws where - Value == [(V1.QueryOutput, repeat (each V2).QueryOutput)], + Value == (V1.QueryOutput, repeat (each V2).QueryOutput), V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load(.fetchAll(statement, database: database)) + try await _sharedReader.load( + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database + ) + ) } } @@ -169,8 +209,14 @@ extension FetchOne { S.From.QueryOutput: Sendable, S.Joins == () { + let statement = statement.selectStar().asSelect() _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database, scheduler: scheduler) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) } @@ -189,8 +235,14 @@ extension FetchOne { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { + let statement = statement.selectStar().asSelect() _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database, scheduler: scheduler) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) } @@ -205,7 +257,12 @@ extension FetchOne { V.QueryOutput: Sendable { _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database, scheduler: scheduler) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) } @@ -223,7 +280,12 @@ extension FetchOne { repeat (each V2).QueryOutput: Sendable { _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database, scheduler: scheduler) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) } @@ -238,7 +300,14 @@ extension FetchOne { S.From.QueryOutput: Sendable, S.Joins == () { - try await _sharedReader.load(.fetchOne(statement, database: database, scheduler: scheduler)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } @_disfavoredOverload @@ -255,7 +324,14 @@ extension FetchOne { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { - try await _sharedReader.load(.fetchOne(statement, database: database, scheduler: scheduler)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } public func load( @@ -267,7 +343,13 @@ extension FetchOne { Value == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load(.fetchOne(statement, database: database, scheduler: scheduler)) + try await _sharedReader.load( + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } @_disfavoredOverload @@ -282,7 +364,13 @@ extension FetchOne { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load(.fetchOne(statement, database: database, scheduler: scheduler)) + try await _sharedReader.load( + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + scheduler: scheduler + ) + ) } } @@ -300,8 +388,14 @@ extension FetchOne { S.From.QueryOutput: Sendable, S.Joins == () { + let statement = statement.selectStar().asSelect() _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database, animation: animation) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + animation: animation + ) ) } @@ -320,8 +414,14 @@ extension FetchOne { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { + let statement = statement.selectStar().asSelect() _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database, animation: animation) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + animation: animation + ) ) } @@ -336,7 +436,12 @@ extension FetchOne { V.QueryOutput: Sendable { _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database, animation: animation) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + animation: animation + ) ) } @@ -354,7 +459,12 @@ extension FetchOne { repeat (each V2).QueryOutput: Sendable { _sharedReader = SharedReader( - wrappedValue: wrappedValue, .fetchOne(statement, database: database, animation: animation) + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + animation: animation + ) ) } @@ -369,7 +479,14 @@ extension FetchOne { S.From.QueryOutput: Sendable, S.Joins == () { - try await _sharedReader.load(.fetchOne(statement, database: database, animation: animation)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + animation: animation + ) + ) } @_disfavoredOverload @@ -386,7 +503,14 @@ extension FetchOne { S.Joins == (repeat each J), repeat (each J).QueryOutput: Sendable { - try await _sharedReader.load(.fetchOne(statement, database: database, animation: animation)) + let statement = statement.selectStar().asSelect() + try await _sharedReader.load( + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + animation: animation + ) + ) } public func load( @@ -398,7 +522,13 @@ extension FetchOne { Value == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load(.fetchOne(statement, database: database, animation: animation)) + try await _sharedReader.load( + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + animation: animation + ) + ) } @_disfavoredOverload @@ -413,7 +543,32 @@ extension FetchOne { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load(.fetchOne(statement, database: database, animation: animation)) + try await _sharedReader.load( + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + animation: animation + ) + ) } } #endif + +private struct FetchOneStatementValueRequest: StatementKeyRequest { + let statement: any StructuredQueriesCore.Statement + func fetch(_ db: Database) throws -> Value.QueryOutput { + guard let result = try statement.fetchOne(db) + else { throw NotFound() } + return result + } +} + +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) +private struct FetchOneStatementPackRequest: StatementKeyRequest { + let statement: any StructuredQueriesCore.Statement<(repeat each Value)> + func fetch(_ db: Database) throws -> (repeat (each Value).QueryOutput) { + guard let result = try statement.fetchOne(db) + else { throw NotFound() } + return result + } +} diff --git a/Sources/SharingGRDBCore/Internal/StatementKey.swift b/Sources/SharingGRDBCore/Internal/StatementKey.swift new file mode 100644 index 00000000..2942ac2f --- /dev/null +++ b/Sources/SharingGRDBCore/Internal/StatementKey.swift @@ -0,0 +1,23 @@ +protocol StatementKeyRequest: FetchKeyRequest { + associatedtype QueryValue + var statement: any StructuredQueriesCore.Statement { get } +} + +extension StatementKeyRequest { + static func == (lhs: Self, rhs: Self) -> Bool { + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // return AnyHashable(lhs.statement) == AnyHashable(rhs.statement) + let lhs = lhs.statement + let rhs = rhs.statement + return AnyHashable(lhs) == AnyHashable(rhs) + } + + func hash(into hasher: inout Hasher) { + // NB: A Swift 6.1 regression prevents this from compiling: + // https://github.com/swiftlang/swift/issues/79623 + // hasher.combine(statement) + let statement = statement + hasher.combine(statement) + } +} diff --git a/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift b/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift deleted file mode 100644 index 1a5cf635..00000000 --- a/Sources/SharingGRDBCore/StructuredQueries/StatementKey.swift +++ /dev/null @@ -1,698 +0,0 @@ -import Dependencies -import Dispatch -import GRDB -import Sharing -import StructuredQueriesCore -import StructuredQueriesGRDBCore - -#if canImport(SwiftUI) - import SwiftUI -#endif - -// MARK: Basics - -extension SharedReaderKey { - /// A key that can query for a collection of data in a SQLite database. - /// - /// This key takes a query built using the StructuredQueries library. - /// - /// ```swift - /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items - /// ``` - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchAll( - _ statement: S, - database: (any DatabaseReader)? = nil - ) -> Self - where - S.QueryValue == (), - S.Joins == (), - Self == FetchKey<[S.From.QueryOutput]>.Default - { - let statement = statement.selectStar() - return fetchAll(statement, database: database) - } - - /// A key that can query for a collection of data in a SQLite database. - /// - /// This key takes a query built using the StructuredQueries library. - /// - /// ```swift - /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items - /// ``` - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchAll( - _ statement: S, - database: (any DatabaseReader)? = nil - ) -> Self - where S.QueryValue: QueryRepresentable, Self == FetchKey<[S.QueryValue.QueryOutput]>.Default { - fetch(FetchAllStatementValueRequest(statement: statement), database: database) - } - - /// A key that can query for a value in a SQLite database. - /// - /// This key takes a query built using the StructuredQueries library. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchOne( - _ statement: S, - database: (any DatabaseReader)? = nil - ) -> Self - where - S.QueryValue == (), - S.Joins == (), - Self == FetchKey - { - let statement = statement.selectStar() - return fetchOne(statement, database: database) - } - - /// A key that can query for a value in a SQLite database. - /// - /// This key takes a query built using the StructuredQueries library. - /// - /// ```swift - /// @SharedReader(.fetchOne(Item.count())) var itemCount = 0 - /// ``` - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement, - database: (any DatabaseReader)? = nil - ) -> Self - where Self == FetchKey { - fetch(FetchOneStatementValueRequest(statement: statement), database: database) - } -} - -// MARK: Parameter pack overloads - -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -extension SharedReaderKey { - /// A key that can query for a collection of data in a SQLite database. - /// - /// This key takes a query built using the StructuredQueries library. - /// - /// ```swift - /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items - /// ``` - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - public static func fetchAll( - _ statement: S, - database: (any DatabaseReader)? = nil - ) -> Self - where - S.QueryValue == (), - S.Joins == (repeat each J), - Self == FetchKey<[(S.From.QueryOutput, repeat (each J).QueryOutput)]>.Default - { - fetchAll(statement.selectStar(), database: database) - } - - /// A key that can query for a collection of data in a SQLite database. - /// - /// This key takes a query built using the StructuredQueries library. - /// - /// ```swift - /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items - /// ``` - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - public static func fetchAll< - S: StructuredQueriesCore.Statement, - V1: QueryRepresentable, - each V2: QueryRepresentable - >( - _ statement: S, - database: (any DatabaseReader)? = nil - ) -> Self - where - S.QueryValue == (V1, repeat each V2), - Self == FetchKey<[(V1.QueryOutput, repeat (each V2).QueryOutput)]>.Default - { - fetch(FetchAllStatementPackRequest(statement: statement), database: database) - } - - /// A key that can query for a value in a SQLite database. - /// - /// This key takes a query built using the StructuredQueries library. - /// - /// ```swift - /// @SharedReader(.fetchAll(Item.order(by: \.name))) var items - /// ``` - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - public static func fetchOne( - _ statement: S, - database: (any DatabaseReader)? = nil - ) -> Self - where - S.QueryValue == (), - S.Joins == (repeat each J), - Self == FetchKey<(S.From.QueryOutput, repeat (each J).QueryOutput)> - { - fetchOne(statement.selectStar(), database: database) - } - - /// A key that can query for a value in a SQLite database. - /// - /// This key takes a query built using the StructuredQueries library. - /// - /// ```swift - /// @SharedReader(.fetchOne(Item.count())) var itemCount = 0 - /// ``` - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, - database: (any DatabaseReader)? = nil - ) -> Self - where Self == FetchKey<(repeat (each Value).QueryOutput)> { - fetch(FetchOneStatementPackRequest(statement: statement), database: database) - } -} - -// MARK: - Scheduling - -extension SharedReaderKey { - /// A key that can query for a collection of data in a SQLite database. - /// - /// A version of `fetchAll` that can be configured with a scheduler. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchAll( - _ statement: S, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where - S.QueryValue == (), - S.Joins == (), - Self == FetchKey<[S.From.QueryOutput]>.Default - { - let statement = statement.selectStar() - return fetchAll(statement, database: database, scheduler: scheduler) - } - - /// A key that can query for a collection of data in a SQLite database. - /// - /// A version of `fetchAll` that can be configured with a scheduler. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchAll( - _ statement: S, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where S.QueryValue: QueryRepresentable, Self == FetchKey<[S.QueryValue.QueryOutput]>.Default { - fetch( - FetchAllStatementValueRequest(statement: statement), database: database, scheduler: scheduler - ) - } - - /// A key that can query for a value in a SQLite database. - /// - /// A version of `fetchOne` that can be configured with a scheduler. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchOne( - _ statement: S, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where - S.QueryValue == (), - S.Joins == (), - Self == FetchKey - { - let statement = statement.selectStar() - return fetchOne(statement, database: database, scheduler: scheduler) - } - - /// A key that can query for a value in a SQLite database. - /// - /// A version of `fetchOne` that can be configured with a scheduler. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where Self == FetchKey { - fetch( - FetchOneStatementValueRequest(statement: statement), database: database, scheduler: scheduler - ) - } -} - -// MARK: Parameter pack overloads - -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -extension SharedReaderKey { - /// A key that can query for a collection of data in a SQLite database. - /// - /// A version of `fetchAll` that can be configured with a scheduler. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - @_documentation(visibility: private) - public static func fetchAll( - _ statement: S, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where - S.QueryValue == (), - S.Joins == (repeat each J), - Self == FetchKey<[(S.From.QueryOutput, repeat (each J).QueryOutput)]>.Default - { - fetchAll(statement.selectStar(), database: database, scheduler: scheduler) - } - - /// A key that can query for a collection of data in a SQLite database. - /// - /// A version of `fetchAll` that can be configured with a scheduler. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - @_documentation(visibility: private) - public static func fetchAll< - S: StructuredQueriesCore.Statement, - V1: QueryRepresentable, - each V2: QueryRepresentable - >( - _ statement: S, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where - S.QueryValue == (V1, repeat each V2), - Self == FetchKey<[(V1.QueryOutput, repeat (each V2).QueryOutput)]>.Default - { - fetch( - FetchAllStatementPackRequest(statement: statement), database: database, scheduler: scheduler - ) - } - - /// A key that can query for a value in a SQLite database. - /// - /// A version of `fetchOne` that can be configured with a scheduler. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - @_documentation(visibility: private) - public static func fetchOne( - _ statement: S, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where - S.QueryValue == (), - S.Joins == (repeat each J), - Self == FetchKey<(S.From.QueryOutput, repeat (each J).QueryOutput)> - { - fetchOne(statement.selectStar(), database: database, scheduler: scheduler) - } - - /// A key that can query for a value in a SQLite database. - /// - /// A version of `fetchOne` that can be configured with a scheduler. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - @_documentation(visibility: private) - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) -> Self - where Self == FetchKey<(repeat (each Value).QueryOutput)> { - fetch( - FetchOneStatementPackRequest(statement: statement), database: database, scheduler: scheduler - ) - } -} - -// MARK: - Animation - -#if canImport(SwiftUI) - extension SharedReaderKey { - /// A key that can query for a collection of data in a SQLite database. - /// - /// A version of `fetchAll` that can be configured with a SwiftUI animation. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchAll( - _ statement: S, - database: (any DatabaseReader)? = nil, - animation: Animation - ) -> Self - where - S.QueryValue == (), - S.Joins == (), - Self == FetchKey<[S.From.QueryOutput]>.Default - { - let statement = statement.selectStar() - return fetchAll(statement, database: database, animation: animation) - } - - /// A key that can query for a collection of data in a SQLite database. - /// - /// A version of `fetchAll` that can be configured with a SwiftUI animation. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchAll( - _ statement: some StructuredQueriesCore.Statement, - database: (any DatabaseReader)? = nil, - animation: Animation - ) -> Self - where Self == FetchKey<[Value.QueryOutput]>.Default { - fetch( - FetchAllStatementValueRequest(statement: statement), - database: database, - animation: animation - ) - } - - /// A key that can query for a value in a SQLite database. - /// - /// A version of `fetchOne` that can be configured with a SwiftUI animation. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchOne( - _ statement: S, - database: (any DatabaseReader)? = nil, - animation: Animation - ) -> Self - where - S.QueryValue == (), - S.Joins == (), - Self == FetchKey - { - let statement = statement.selectStar() - return fetchOne(statement, database: database, animation: animation) - } - - /// A key that can query for a collection of value in a SQLite database. - /// - /// A version of `fetchOne` that can be configured with a SwiftUI animation. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement, - database: (any DatabaseReader)? = nil, - animation: Animation - ) -> Self - where Self == FetchKey { - fetch( - FetchOneStatementValueRequest(statement: statement), - database: database, - animation: animation - ) - } - } - - // MARK: Parameter pack overloads - - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - extension SharedReaderKey { - /// A key that can query for a collection of data in a SQLite database. - /// - /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a - /// SwiftUI animation. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - @_documentation(visibility: private) - public static func fetchAll( - _ statement: S, - database: (any DatabaseReader)? = nil, - animation: Animation - ) -> Self - where - S.QueryValue == (), - S.Joins == (repeat each J), - Self == FetchKey<[(S.From.QueryOutput, repeat (each J).QueryOutput)]>.Default - { - fetchAll(statement.selectStar(), database: database, animation: animation) - } - - /// A key that can query for a collection of data in a SQLite database. - /// - /// A version of ``Sharing/SharedReaderKey/fetchAll(_:database:)`` that can be configured with a - /// SwiftUI animation. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - @_documentation(visibility: private) - public static func fetchAll( - _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, - database: (any DatabaseReader)? = nil, - animation: Animation - ) -> Self - where Self == FetchKey<[(repeat (each Value).QueryOutput)]>.Default { - fetch( - FetchAllStatementPackRequest(statement: statement), - database: database, - animation: animation - ) - } - - /// A key that can query for a value in a SQLite database. - /// - /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a - /// SwiftUI animation. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - @_documentation(visibility: private) - public static func fetchOne( - _ statement: S, - database: (any DatabaseReader)? = nil, - animation: Animation - ) -> Self - where - S.QueryValue == (), - S.Joins == (repeat each J), - Self == FetchKey<(S.From.QueryOutput, repeat (each J).QueryOutput)> - { - fetchOne(statement.selectStar(), database: database, animation: animation) - } - - /// A key that can query for a value in a SQLite database. - /// - /// A version of ``Sharing/SharedReaderKey/fetchOne(_:database:)`` that can be configured with a - /// SwiftUI animation. - /// - /// - Parameters: - /// - statement: A structured query describing the data to be fetched. - /// - database: The database to read from. A value of `nil` will use the default database. - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. - @_disfavoredOverload - @_documentation(visibility: private) - public static func fetchOne( - _ statement: some StructuredQueriesCore.Statement<(repeat each Value)>, - database: (any DatabaseReader)? = nil, - animation: Animation - ) -> Self - where Self == FetchKey<(repeat (each Value).QueryOutput)> { - fetch( - FetchOneStatementPackRequest(statement: statement), - database: database, - animation: animation - ) - } - } -#endif - -// MARK: - - -private struct FetchAllStatementValueRequest: StatementKeyRequest { - let statement: any StructuredQueriesCore.Statement - func fetch(_ db: Database) throws -> [Value.QueryOutput] { - try statement.fetchAll(db) - } -} - -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -private struct FetchAllStatementPackRequest: StatementKeyRequest { - let statement: any StructuredQueriesCore.Statement<(repeat each Value)> - func fetch(_ db: Database) throws -> [(repeat (each Value).QueryOutput)] { - try statement.fetchAll(db) - } -} - -private struct FetchOneStatementValueRequest: StatementKeyRequest { - let statement: any StructuredQueriesCore.Statement - func fetch(_ db: Database) throws -> Value.QueryOutput { - guard let result = try statement.fetchOne(db) - else { throw NotFound() } - return result - } -} - -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -private struct FetchOneStatementPackRequest: StatementKeyRequest { - let statement: any StructuredQueriesCore.Statement<(repeat each Value)> - func fetch(_ db: Database) throws -> (repeat (each Value).QueryOutput) { - guard let result = try statement.fetchOne(db) - else { throw NotFound() } - return result - } -} - -private protocol StatementKeyRequest: FetchKeyRequest { - associatedtype QueryValue - var statement: any StructuredQueriesCore.Statement { get } -} - -extension StatementKeyRequest { - static func == (lhs: Self, rhs: Self) -> Bool { - // NB: A Swift 6.1 regression prevents this from compiling: - // https://github.com/swiftlang/swift/issues/79623 - // return AnyHashable(lhs.statement) == AnyHashable(rhs.statement) - let lhs = lhs.statement - let rhs = rhs.statement - return AnyHashable(lhs) == AnyHashable(rhs) - } - - func hash(into hasher: inout Hasher) { - // NB: A Swift 6.1 regression prevents this from compiling: - // https://github.com/swiftlang/swift/issues/79623 - // hasher.combine(statement) - let statement = statement - hasher.combine(statement) - } -} From 2a9787c4589b2cfa89f709ca261d1153bea8c388 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 16:40:35 -0700 Subject: [PATCH 154/171] wip --- Examples/CaseStudies/UIKitDemo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/CaseStudies/UIKitDemo.swift b/Examples/CaseStudies/UIKitDemo.swift index 93951d25..88951175 100644 --- a/Examples/CaseStudies/UIKitDemo.swift +++ b/Examples/CaseStudies/UIKitDemo.swift @@ -13,7 +13,7 @@ final class UIKitCaseStudyViewController: UICollectionViewController, UIKitCaseS """ private var dataSource: UICollectionViewDiffableDataSource! - @SharedReader(.fetchAll(Fact.order { $0.id.desc() }, animation: .default)) + @FetchAll(Fact.order { $0.id.desc() }, animation: .default) private var facts private var viewDidLoadTask: Task? From 4e2f77fbe241b1f6f6d0a8f5a897fcdd6f7fe2b8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 17:17:03 -0700 Subject: [PATCH 155/171] wip --- Examples/Reminders/ReminderRow.swift | 3 +- Examples/Reminders/Schema.swift | 2 +- Package.swift | 2 +- Sources/SharingGRDBCore/Fetch.swift | 4 +- Sources/SharingGRDBCore/FetchAll.swift | 294 ++++++++- Sources/SharingGRDBCore/FetchKey.swift | 8 +- Sources/SharingGRDBCore/FetchOne.swift | 592 ++++++++++++------ .../QueryCursor.swift | 3 +- 8 files changed, 682 insertions(+), 226 deletions(-) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index 595503a3..479a420f 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -127,7 +127,8 @@ struct ReminderRow: View { private func toggleCompletion() { withErrorReporting { try database.write { db in - isCompleted = try Reminder + isCompleted = + try Reminder .find(reminder.id) .update { $0.isCompleted.toggle() } .returning(\.isCompleted) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 13916f9b..601d060f 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -29,7 +29,7 @@ extension Reminder { static func searching(_ text: String) -> Where { Self.where { $0.title.collate(.nocase).contains(text) - || $0.notes.collate(.nocase).contains(text) + || $0.notes.collate(.nocase).contains(text) } } static let withTags = group(by: \.id) diff --git a/Package.swift b/Package.swift index 70cbad4c..b00ec79f 100644 --- a/Package.swift +++ b/Package.swift @@ -82,7 +82,7 @@ let package = Package( .product(name: "DependenciesTestSupport", package: "swift-dependencies"), .product(name: "StructuredQueries", package: "swift-structured-queries"), ] - ) + ), ], swiftLanguageModes: [.v6] ) diff --git a/Sources/SharingGRDBCore/Fetch.swift b/Sources/SharingGRDBCore/Fetch.swift index 7a92103a..f76abab9 100644 --- a/Sources/SharingGRDBCore/Fetch.swift +++ b/Sources/SharingGRDBCore/Fetch.swift @@ -1,5 +1,5 @@ #if canImport(Combine) -import Combine + import Combine #endif #if canImport(SwiftUI) import SwiftUI @@ -16,7 +16,7 @@ public struct Fetch: Sendable { public var projectedValue: Self { self } - + public var loadError: (any Error)? { _sharedReader.loadError } diff --git a/Sources/SharingGRDBCore/FetchAll.swift b/Sources/SharingGRDBCore/FetchAll.swift index 4a6ea770..3733665b 100644 --- a/Sources/SharingGRDBCore/FetchAll.swift +++ b/Sources/SharingGRDBCore/FetchAll.swift @@ -1,40 +1,66 @@ #if canImport(Combine) -import Combine + import Combine #endif #if canImport(SwiftUI) import SwiftUI #endif +/// A property that can query for a collection of data in a SQLite database. +/// +/// It takes a query built using the StructuredQueries library: +/// +/// ```swift +/// @FetchAll(Item.order(by: \.name)) var items +/// ``` @propertyWrapper -public struct FetchAll: Sendable { - public var _sharedReader: SharedReader<[Element]> = SharedReader(value: []) +public struct FetchAll: Sendable { + private var sharedReader: SharedReader<[Element]> = SharedReader(value: []) + /// A collection of data associated with the underlying query. public var wrappedValue: [Element] { - _sharedReader.wrappedValue + sharedReader.wrappedValue } + /// Returns this property wrapper. + /// + /// Useful if you want to access various property wrapper state, like ``loadError``, + /// ``isLoading``, and ``publisher``. public var projectedValue: Self { self } + /// An error encountered during the most recent attempt to load data. public var loadError: (any Error)? { - _sharedReader.loadError + sharedReader.loadError } + /// Whether or not data is loading from the database. public var isLoading: Bool { - _sharedReader.isLoading + sharedReader.isLoading } #if canImport(Combine) + /// A publisher that emits events when the database observes changes to the query. public var publisher: some Publisher<[Element], Never> { - _sharedReader.publisher + sharedReader.publisher } #endif - public init() { - _sharedReader = SharedReader(value: []) + /// Initializes this property with a query that fetches every row from a table. + public init( + database: (any DatabaseReader)? = nil + ) + where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { + let statement = Element.all.selectStar().asSelect() + self.init(statement, database: database) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). public init( _ statement: S, database: (any DatabaseReader)? = nil @@ -46,7 +72,7 @@ public struct FetchAll: Sendable { S.Joins == () { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementValueRequest(statement: statement), database: database @@ -54,6 +80,12 @@ public struct FetchAll: Sendable { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -68,11 +100,17 @@ public struct FetchAll: Sendable { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch(FetchAllStatementPackRequest(statement: statement), database: database) ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). public init( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil @@ -81,7 +119,7 @@ public struct FetchAll: Sendable { Element == V.QueryOutput, V.QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementValueRequest(statement: statement), database: database @@ -89,6 +127,12 @@ public struct FetchAll: Sendable { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -100,7 +144,7 @@ public struct FetchAll: Sendable { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementPackRequest(statement: statement), database: database @@ -108,6 +152,12 @@ public struct FetchAll: Sendable { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). public func load( _ statement: S, database: (any DatabaseReader)? = nil @@ -119,7 +169,7 @@ public struct FetchAll: Sendable { S.Joins == () { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementValueRequest(statement: statement), database: database @@ -127,6 +177,12 @@ public struct FetchAll: Sendable { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -141,7 +197,7 @@ public struct FetchAll: Sendable { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementPackRequest(statement: statement), database: database @@ -149,6 +205,12 @@ public struct FetchAll: Sendable { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). public func load( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil @@ -157,7 +219,7 @@ public struct FetchAll: Sendable { Element == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementValueRequest(statement: statement), database: database @@ -165,6 +227,12 @@ public struct FetchAll: Sendable { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -176,7 +244,7 @@ public struct FetchAll: Sendable { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementPackRequest(statement: statement), database: database @@ -186,6 +254,30 @@ public struct FetchAll: Sendable { } extension FetchAll { + /// Initializes this property with a query that fetches every row from a table. + /// + /// - Parameters: + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + public init( + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) + where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { + let statement = Element.all.selectStar().asSelect() + self.init(statement, database: database, scheduler: scheduler) + } + + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public init( _ statement: S, database: (any DatabaseReader)? = nil, @@ -198,7 +290,7 @@ extension FetchAll { S.Joins == () { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -207,6 +299,14 @@ extension FetchAll { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -222,7 +322,7 @@ extension FetchAll { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementPackRequest(statement: statement), database: database, @@ -231,6 +331,14 @@ extension FetchAll { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public init( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, @@ -240,7 +348,7 @@ extension FetchAll { Element == V.QueryOutput, V.QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -249,6 +357,14 @@ extension FetchAll { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -261,7 +377,7 @@ extension FetchAll { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementPackRequest(statement: statement), database: database, @@ -270,6 +386,14 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public func load( _ statement: S, database: (any DatabaseReader)? = nil, @@ -282,7 +406,7 @@ extension FetchAll { S.Joins == () { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -291,6 +415,14 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -306,7 +438,7 @@ extension FetchAll { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementPackRequest(statement: statement), database: database, @@ -315,6 +447,14 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public func load( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, @@ -324,7 +464,7 @@ extension FetchAll { Element == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -333,6 +473,14 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -345,7 +493,7 @@ extension FetchAll { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementPackRequest(statement: statement), database: database @@ -356,6 +504,30 @@ extension FetchAll { #if canImport(SwiftUI) extension FetchAll { + /// Initializes this property with a query that fetches every row from a table. + /// + /// - Parameters: + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + public init( + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { + let statement = Element.all.selectStar().asSelect() + self.init(statement, database: database, animation: animation) + } + + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. public init( _ statement: S, database: (any DatabaseReader)? = nil, @@ -368,7 +540,7 @@ extension FetchAll { S.Joins == () { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -377,6 +549,14 @@ extension FetchAll { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -392,7 +572,7 @@ extension FetchAll { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementPackRequest(statement: statement), database: database, @@ -401,6 +581,14 @@ extension FetchAll { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. public init( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, @@ -410,7 +598,7 @@ extension FetchAll { Element == V.QueryOutput, V.QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -419,6 +607,14 @@ extension FetchAll { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -431,7 +627,7 @@ extension FetchAll { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( .fetch( FetchAllStatementPackRequest(statement: statement), database: database, @@ -440,6 +636,14 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. public func load( _ statement: S, database: (any DatabaseReader)? = nil, @@ -452,7 +656,7 @@ extension FetchAll { S.Joins == () { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -461,6 +665,14 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -476,7 +688,7 @@ extension FetchAll { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementPackRequest(statement: statement), database: database, @@ -485,6 +697,14 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. public func load( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, @@ -494,7 +714,7 @@ extension FetchAll { Element == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -503,6 +723,14 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -515,7 +743,7 @@ extension FetchAll { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchAllStatementPackRequest(statement: statement), database: database, diff --git a/Sources/SharingGRDBCore/FetchKey.swift b/Sources/SharingGRDBCore/FetchKey.swift index 1dd64c0e..5438c365 100644 --- a/Sources/SharingGRDBCore/FetchKey.swift +++ b/Sources/SharingGRDBCore/FetchKey.swift @@ -199,7 +199,9 @@ extension SharedReaderKey { ) -> Self where Self == FetchKey<[Record]>.Default { Self[ - .fetch(FetchAllRequest(sql: sql, arguments: arguments), database: database, scheduler: scheduler), + .fetch( + FetchAllRequest(sql: sql, arguments: arguments), database: database, scheduler: scheduler + ), default: [] ] } @@ -225,7 +227,9 @@ extension SharedReaderKey { scheduler: some ValueObservationScheduler & Hashable ) -> Self where Self == FetchKey { - .fetch(FetchOneRequest(sql: sql, arguments: arguments), database: database, scheduler: scheduler) + .fetch( + FetchOneRequest(sql: sql, arguments: arguments), database: database, scheduler: scheduler + ) } } diff --git a/Sources/SharingGRDBCore/FetchOne.swift b/Sources/SharingGRDBCore/FetchOne.swift index 4bae7724..f83df9d6 100644 --- a/Sources/SharingGRDBCore/FetchOne.swift +++ b/Sources/SharingGRDBCore/FetchOne.swift @@ -1,40 +1,81 @@ #if canImport(Combine) -import Combine + import Combine #endif #if canImport(SwiftUI) import SwiftUI #endif +/// A property that can query for a value in a SQLite database. +/// +/// It takes a query built using the StructuredQueries library: +/// +/// ```swift +/// @FetchOne(Item.count) var itemsCount = 0 +/// ``` @propertyWrapper public struct FetchOne: Sendable { - public var _sharedReader: SharedReader + private var sharedReader: SharedReader + /// A value associated with the underlying query. public var wrappedValue: Value { - _sharedReader.wrappedValue + sharedReader.wrappedValue } + /// Returns this property wrapper. + /// + /// Useful if you want to access various property wrapper state, like ``loadError``, + /// ``isLoading``, and ``publisher``. public var projectedValue: Self { self } + /// An error encountered during the most recent attempt to load data. public var loadError: (any Error)? { - _sharedReader.loadError + sharedReader.loadError } + /// Whether or not data is loading from the database. public var isLoading: Bool { - _sharedReader.isLoading + sharedReader.isLoading } #if canImport(Combine) + /// A publisher that emits events when the database observes changes to the query. public var publisher: some Publisher { - _sharedReader.publisher + sharedReader.publisher } #endif - public init(wrappedValue: sending Value) { - _sharedReader = SharedReader(value: wrappedValue) + /// Initializes this property with a wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + public init( + wrappedValue: sending Value, + database: (any DatabaseReader)? = nil + ) { + sharedReader = SharedReader(value: wrappedValue) } + /// Initializes this property with a wrapped value. + /// + /// - Parameters database: The database to read from. A value of `nil` will use the default + /// database (`@Dependency(\.defaultDatabase)`). + public init( + database: (any DatabaseReader)? = nil + ) where Value == Wrapped? { + self.init(wrappedValue: nil, database: database) + } + + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). public init( wrappedValue: S.From.QueryOutput, _ statement: S, @@ -47,7 +88,7 @@ public struct FetchOne: Sendable { S.Joins == () { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch( FetchOneStatementValueRequest(statement: statement), @@ -56,6 +97,13 @@ public struct FetchOne: Sendable { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -71,7 +119,7 @@ public struct FetchOne: Sendable { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch( FetchOneStatementPackRequest(statement: statement), @@ -80,6 +128,13 @@ public struct FetchOne: Sendable { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). public init( wrappedValue: V.QueryOutput, _ statement: some StructuredQueriesCore.Statement, @@ -89,7 +144,7 @@ public struct FetchOne: Sendable { Value == V.QueryOutput, V.QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch( FetchOneStatementValueRequest(statement: statement), @@ -98,6 +153,13 @@ public struct FetchOne: Sendable { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -110,7 +172,7 @@ public struct FetchOne: Sendable { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch( FetchOneStatementPackRequest(statement: statement), @@ -119,6 +181,12 @@ public struct FetchOne: Sendable { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). public func load( _ statement: S, database: (any DatabaseReader)? = nil @@ -130,7 +198,7 @@ public struct FetchOne: Sendable { S.Joins == () { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchOneStatementValueRequest(statement: statement), database: database @@ -138,6 +206,12 @@ public struct FetchOne: Sendable { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -152,7 +226,7 @@ public struct FetchOne: Sendable { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchOneStatementPackRequest(statement: statement), database: database @@ -160,6 +234,12 @@ public struct FetchOne: Sendable { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). public func load( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil @@ -168,7 +248,7 @@ public struct FetchOne: Sendable { Value == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchOneStatementValueRequest(statement: statement), database: database @@ -176,6 +256,12 @@ public struct FetchOne: Sendable { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -187,7 +273,7 @@ public struct FetchOne: Sendable { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchOneStatementPackRequest(statement: statement), database: database @@ -196,6 +282,15 @@ public struct FetchOne: Sendable { } } +/// Initializes this property with a query associated with the wrapped value. +/// +/// - Parameters: +/// - wrappedValue: A default value to associate with this property. +/// - statement: A query associated with the wrapped value. +/// - database: The database to read from. A value of `nil` will use the default database +/// (`@Dependency(\.defaultDatabase)`). +/// - scheduler: The scheduler to observe from. By default, database observation is performed +/// asynchronously on the main queue. extension FetchOne { public init( wrappedValue: S.From.QueryOutput, @@ -210,7 +305,7 @@ extension FetchOne { S.Joins == () { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch( FetchOneStatementValueRequest(statement: statement), @@ -220,6 +315,15 @@ extension FetchOne { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -236,7 +340,7 @@ extension FetchOne { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch( FetchOneStatementPackRequest(statement: statement), @@ -246,6 +350,15 @@ extension FetchOne { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public init( wrappedValue: V.QueryOutput, _ statement: some StructuredQueriesCore.Statement, @@ -256,7 +369,7 @@ extension FetchOne { Value == V.QueryOutput, V.QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch( FetchOneStatementValueRequest(statement: statement), @@ -266,6 +379,15 @@ extension FetchOne { ) } + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( @@ -279,7 +401,7 @@ extension FetchOne { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch( FetchOneStatementPackRequest(statement: statement), @@ -289,6 +411,14 @@ extension FetchOne { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public func load( _ statement: S, database: (any DatabaseReader)? = nil, @@ -301,7 +431,7 @@ extension FetchOne { S.Joins == () { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchOneStatementValueRequest(statement: statement), database: database, @@ -310,6 +440,14 @@ extension FetchOne { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -325,7 +463,7 @@ extension FetchOne { repeat (each J).QueryOutput: Sendable { let statement = statement.selectStar().asSelect() - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchOneStatementPackRequest(statement: statement), database: database, @@ -334,6 +472,14 @@ extension FetchOne { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public func load( _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, @@ -343,7 +489,7 @@ extension FetchOne { Value == V.QueryOutput, V.QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchOneStatementValueRequest(statement: statement), database: database, @@ -352,6 +498,14 @@ extension FetchOne { ) } + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public func load( @@ -364,7 +518,7 @@ extension FetchOne { V1.QueryOutput: Sendable, repeat (each V2).QueryOutput: Sendable { - try await _sharedReader.load( + try await sharedReader.load( .fetch( FetchOneStatementPackRequest(statement: statement), database: database, @@ -375,183 +529,251 @@ extension FetchOne { } #if canImport(SwiftUI) -extension FetchOne { - public init( - wrappedValue: S.From.QueryOutput, - _ statement: S, - database: (any DatabaseReader)? = nil, - animation: Animation - ) - where - Value == S.From.QueryOutput, - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == () - { - let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( - wrappedValue: wrappedValue, - .fetch( - FetchOneStatementValueRequest(statement: statement), - database: database, - animation: animation - ) + extension FetchOne { + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + public init( + wrappedValue: S.From.QueryOutput, + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation ) - } - - @_disfavoredOverload - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public init( - wrappedValue: (S.From.QueryOutput, repeat (each J).QueryOutput), - _ statement: S, - database: (any DatabaseReader)? = nil, - animation: Animation - ) - where - Value == (S.From.QueryOutput, repeat (each J).QueryOutput), - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == (repeat each J), - repeat (each J).QueryOutput: Sendable - { - let statement = statement.selectStar().asSelect() - _sharedReader = SharedReader( - wrappedValue: wrappedValue, - .fetch( - FetchOneStatementPackRequest(statement: statement), - database: database, - animation: animation + where + Value == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + let statement = statement.selectStar().asSelect() + sharedReader = SharedReader( + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + animation: animation + ) ) - ) - } + } - public init( - wrappedValue: V.QueryOutput, - _ statement: some StructuredQueriesCore.Statement, - database: (any DatabaseReader)? = nil, - animation: Animation - ) - where - Value == V.QueryOutput, - V.QueryOutput: Sendable - { - _sharedReader = SharedReader( - wrappedValue: wrappedValue, - .fetch( - FetchOneStatementValueRequest(statement: statement), - database: database, - animation: animation - ) + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: (S.From.QueryOutput, repeat (each J).QueryOutput), + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation ) - } + where + Value == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + let statement = statement.selectStar().asSelect() + sharedReader = SharedReader( + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + animation: animation + ) + ) + } - @_disfavoredOverload - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public init( - wrappedValue: (V1.QueryOutput, repeat (each V2).QueryOutput), - _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, - database: (any DatabaseReader)? = nil, - animation: Animation - ) - where - Value == (V1.QueryOutput, repeat (each V2).QueryOutput), - V1.QueryOutput: Sendable, - repeat (each V2).QueryOutput: Sendable - { - _sharedReader = SharedReader( - wrappedValue: wrappedValue, - .fetch( - FetchOneStatementPackRequest(statement: statement), - database: database, - animation: animation + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + public init( + wrappedValue: V.QueryOutput, + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Value == V.QueryOutput, + V.QueryOutput: Sendable + { + sharedReader = SharedReader( + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + animation: animation + ) ) + } + + /// Initializes this property with a query associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: (V1.QueryOutput, repeat (each V2).QueryOutput), + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + animation: Animation ) - } + where + Value == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + sharedReader = SharedReader( + wrappedValue: wrappedValue, + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + animation: animation + ) + ) + } - public func load( - _ statement: S, - database: (any DatabaseReader)? = nil, - animation: Animation - ) async throws - where - Value == S.From.QueryOutput, - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == () - { - let statement = statement.selectStar().asSelect() - try await _sharedReader.load( - .fetch( - FetchOneStatementValueRequest(statement: statement), - database: database, - animation: animation + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Value == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + let statement = statement.selectStar().asSelect() + try await sharedReader.load( + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + animation: animation + ) ) - ) - } + } - @_disfavoredOverload - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public func load( - _ statement: S, - database: (any DatabaseReader)? = nil, - animation: Animation - ) async throws - where - Value == (S.From.QueryOutput, repeat (each J).QueryOutput), - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == (repeat each J), - repeat (each J).QueryOutput: Sendable - { - let statement = statement.selectStar().asSelect() - try await _sharedReader.load( - .fetch( - FetchOneStatementPackRequest(statement: statement), - database: database, - animation: animation + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: S, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Value == (S.From.QueryOutput, repeat (each J).QueryOutput), + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == (repeat each J), + repeat (each J).QueryOutput: Sendable + { + let statement = statement.selectStar().asSelect() + try await sharedReader.load( + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + animation: animation + ) ) - ) - } + } - public func load( - _ statement: some StructuredQueriesCore.Statement, - database: (any DatabaseReader)? = nil, - animation: Animation - ) async throws - where - Value == V.QueryOutput, - V.QueryOutput: Sendable - { - try await _sharedReader.load( - .fetch( - FetchOneStatementValueRequest(statement: statement), - database: database, - animation: animation + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + public func load( + _ statement: some StructuredQueriesCore.Statement, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Value == V.QueryOutput, + V.QueryOutput: Sendable + { + try await sharedReader.load( + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + animation: animation + ) ) - ) - } + } - @_disfavoredOverload - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public func load( - _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, - database: (any DatabaseReader)? = nil, - animation: Animation - ) async throws - where - Value ==(V1.QueryOutput, repeat (each V2).QueryOutput), - V1.QueryOutput: Sendable, - repeat (each V2).QueryOutput: Sendable - { - try await _sharedReader.load( - .fetch( - FetchOneStatementPackRequest(statement: statement), - database: database, - animation: animation + /// Replaces the wrapped value with data from the given query. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + @_disfavoredOverload + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public func load( + _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, + database: (any DatabaseReader)? = nil, + animation: Animation + ) async throws + where + Value == (V1.QueryOutput, repeat (each V2).QueryOutput), + V1.QueryOutput: Sendable, + repeat (each V2).QueryOutput: Sendable + { + try await sharedReader.load( + .fetch( + FetchOneStatementPackRequest(statement: statement), + database: database, + animation: animation + ) ) - ) + } } -} #endif private struct FetchOneStatementValueRequest: StatementKeyRequest { diff --git a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift index a79a1978..c2782d73 100644 --- a/Sources/StructuredQueriesGRDBCore/QueryCursor.swift +++ b/Sources/StructuredQueriesGRDBCore/QueryCursor.swift @@ -68,7 +68,8 @@ final class QueryVoidCursor: QueryCursor { extension Database { @inlinable func prepare(query: QueryFragment) throws -> (GRDB.Statement, SQLiteQueryDecoder) { - let queryString = query.isEmpty + let queryString = + query.isEmpty ? "SELECT 1 WHERE 0 -- Empty query generated by StructuredQueries" : query.string let statement = try makeStatement(sql: queryString) From fd8ca4c05827557b005d29628afa7b55f019b386 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 17:23:30 -0700 Subject: [PATCH 156/171] wip --- Examples/CaseStudies/TransactionDemo.swift | 3 +- MigrationGuide0.2.md | 318 ++++++++++++++++++ README.md | 19 +- .../contents.xcworkspacedata | 3 + .../Documentation.docc/SharingGRDB.md | 2 +- .../Articles/ComparisonWithSwiftData.md | 56 +-- .../Articles/DynamicQueries.md | 8 +- .../Documentation.docc/Articles/Fetching.md | 19 +- .../Articles/MigrationGuides.md | 17 + .../MigrationGuides/MigratingTo0.2.md | 21 ++ 10 files changed, 430 insertions(+), 36 deletions(-) create mode 100644 MigrationGuide0.2.md create mode 100644 Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides.md create mode 100644 Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md diff --git a/Examples/CaseStudies/TransactionDemo.swift b/Examples/CaseStudies/TransactionDemo.swift index 32a559c0..ddb629b7 100644 --- a/Examples/CaseStudies/TransactionDemo.swift +++ b/Examples/CaseStudies/TransactionDemo.swift @@ -15,7 +15,8 @@ struct TransactionDemo: SwiftUICaseStudy { """ let caseStudyTitle = "Database Transactions" - @SharedReader(.fetch(Facts(), animation: .default)) private var facts = Facts.Value() + @Fetch(Facts(), animation: .default) + private var facts = Facts.Value() @Dependency(\.defaultDatabase) var database diff --git a/MigrationGuide0.2.md b/MigrationGuide0.2.md new file mode 100644 index 00000000..acb19f7d --- /dev/null +++ b/MigrationGuide0.2.md @@ -0,0 +1,318 @@ +# Migrating to 1.4 + +Update your code to make use of the ``Reducer()`` macro, and learn how to better leverage case key +paths in your features. + +## Overview + +The Composable Architecture is under constant development, and we are always looking for ways to +simplify the library, and make it more powerful. As such, we often need to deprecate certain APIs +in favor of newer ones. We recommend people update their code as quickly as possible to the newest +APIs, and this article contains some tips for doing so. + +* [Using the @Reducer macro](#Using-the-Reducer-macro) +* [Using case key paths](#Using-case-key-paths) +* [Receiving test store actions](#Receiving-test-store-actions) +* [Moving off of `TaskResult`](#Moving-off-of-TaskResult) +* [Identified actions](#Identified-actions) + +### Using the @Reducer macro + +Version 1.4 of the library has introduced a new macro for automating certain aspects of implementing +a ``Reducer``. It is called ``Reducer()``, and to migrate existing code one only needs to annotate +their type with `@Reducer`: + +```diff ++@Reducer + struct MyFeature: Reducer { + // ... + } +``` + +No other changes to be made, and you can immediately start taking advantage of new capabilities of +reducer composition, such as case key paths (see guides below). See the documentation of +``Reducer()`` to see everything that macro adds to your feature's reducer. + +You can also technically drop the ``Reducer`` conformance: + +```diff + @Reducer +-struct MyFeature: Reducer { ++struct MyFeature { + // ... + } +``` + +However, there are some known issues in Xcode that cause autocomplete and type inference to break. +See the documentation of for more gotchas on using the `@Reducer` macro. + + +### Using case key paths + +In version 1.4 we soft-deprecated many APIs that take the `CasePath` type in favor of APIs that take +what is known as a `CaseKeyPath`. Both of these types come from our [CasePaths][swift-case-paths] +library and aim to allow one to abstract over the shape of enums just as key paths allow one to do +so with structs. + +However, in conjunction with version 1.4 of this library we also released an update to CasePaths +that massively improved the ergonomics of using case paths. We introduced the `@CasePathable` macro +for automatically deriving case paths so that we could stop using runtime reflection, and we +introduced a way of using key paths to describe case paths. And so the old `CasePath` type has been +deprecated, and the new `CaseKeyPath` type has taken its place. + +This means that previously when you would use APIs involving case paths you would have to use the +`/` prefix operator to derive the case path. For example: + +```swift +Reduce { state, action in + // ... +} +.ifLet(\.child, action: /Action.child) { + ChildFeature() +} +``` + +You now get to shorten that into a far simpler, more familiar key path syntax: + +```swift +Reduce { state, action in + // ... +} +.ifLet(\.child, action: \.child) { + ChildFeature() +} +``` + +To be able to take advantage of this syntax with your feature's actions, you must annotate your +``Reducer`` conformances with the ``Reducer()`` macro: + +```swift +@Reducer +struct Feature { + // ... +} +``` + +Which automatically applies the `@CasePathable` macro to the feature's `Action` enum among other +things: + +```diff ++@CasePathable + enum Action { + // ... + } +``` + +Further, if the feature's `State` is an enum, `@CasePathable` will also be applied, along with +`@dynamicMemberLookup`: + +```diff ++@CasePathable ++@dynamicMemberLookup + enum State { + // ... + } +``` + +Dynamic member lookups allows a state's associated value to be accessed via dot-syntax, which can be +useful when scoping a store's state to a specific case: + +```diff + IfLetStore( + store.scope( +- state: /Feature.State.tray, action: Feature.Action.tray ++ state: \.tray, action: { .tray($0) } + ) +) { store in + // ... +} +``` + +To form a case key path for any other enum, you must apply the `@CasePathable` macro explicitly: + +```swift +@CasePathable +enum DelegateAction { + case didFinish(success: Bool) +} +``` + +And to access its associated values, you must also apply the `@dynamicMemberLookup` attributes: + +```swift +@CasePathable +@dynamicMemberLookup +enum DestinationState { + case tray(Tray.State) +} +``` + +Anywhere you previously used the `/` prefix operator for case paths you should now be able to use +key path syntax, so long as all of the enums involved are `@CasePathable`. + +If you encounter any problems, create a [discussion][tca-discussions] on the Composable Architecture +repo. + +### Receiving test store actions + +The power of case key paths and the `@CasePathable` macro has made it possible to massively simplify +how one asserts on actions received in a ``TestStore``. Instead of constructing the concrete action +received from an effect like this: + +```swift +store.receive(.child(.presented(.response(.success("Hello!"))))) +``` + +…you can use key path syntax to describe the nesting of action cases that is received: + +```swift +store.receive(\.child.presented.response.success) +``` + +> Note: Case key path syntax requires that every nested action is `@CasePathable`. Reducer actions +> are typically `@CasePathable` automatically via the ``Reducer()`` macro, but other enums must be +> explicitly annotated: +> +> ```swift +> @CasePathable +> enum DelegateAction { +> case didFinish(success: Bool) +> } +> ``` + +And in the case of ``PresentationAction`` you can even omit the ``PresentationAction/presented(_:)`` +path component: + +```swift +store.receive(\.child.response.success) +``` + +This does not assert on the _data_ received in the action, but typically that is already covered +by the state assertion made inside the trailing closure of `receive`. And if you use this style of +action receiving exclusively, you can even stop conforming your action types to `Equatable`. + +There are a few advanced situations to be aware of. When receiving an action that involves an +``IdentifiedAction`` (more information below in ), then +you can use the subscript ``IdentifiedAction/AllCasePaths-swift.struct/subscript(id:)`` to +receive a particular action for an element: + +```swift +store.receive(\.rows[id: 0].response.success) +``` + +And the same goes for ``StackAction`` too: + +```swift +store.receive(\.path[id: 0].response.success) +``` + +### Moving off of TaskResult + +In version 1.4 of the library, the ``TaskResult`` was soft-deprecated and eventually will be fully +deprecated and then removed. The original rationale for the introduction of ``TaskResult`` was to +make an equatable-friendly version of `Result` for when the error produced was `any Error`, which is +not equatable. And the reason to want an equatable-friendly result is so that the `Action` type in +reducers can be equatable, and the reason for _that_ is to make it possible to test actions +emitted by effects. + +Typically in tests, when one wants to assert that the ``TestStore`` received an action you must +specify a concrete action: + +```swift +store.receive(.response(.success("Hello!"))) { + // ... +} +``` + +The ``TestStore`` uses the equatable conformance of `Action` to confirm that you are asserting that +the store received the correct action. + +However, this becomes verbose when testing deeply nested features, which is common in integration +tests: + +```swift +store.receive(.child(.response(.success("Hello!")))) { + // ... +} +``` + +However, with the introduction of [case key paths][swift-case-paths] we greatly improved the +ergonomics of referring to deeply nested enums. You can now use key path syntax to describe the +case of the enum you expect to receive, and you can even omit the associated data from the action +since typically that is covered in the state assertion: + +```swift +store.receive(\.child.response.success) { + // ... +} +``` + +And this syntax does not require the `Action` enum to be equatable since we are only asserting that +the case of the action was received. We are not testing the data in the action. + +We feel that with this better syntax there is less of a reason to have ``TaskResult`` and so we +do plan on removing it eventually. If you have an important use case for ``TaskResult`` that you +think merits it being in the library, please [open a discussion][tca-discussions]. + +### Identified actions + +In version 1.4 of the library we introduced the ``IdentifiedAction`` type which makes it more +ergonomic to bundle the data needed for actions in collections of data. Previously you would +have a case in your `Action` enum for a particular row that holds the ID of the state being acted +upon as well as the action: + +```swift +enum Action { + // ... + case row(id: State.ID, action: Action) +} +``` + +This can be updated to hold onto ``IdentifiedAction`` instead of those piece of data directly in the +case: + +```swift +enum Action { + // ... + case rows(IdentifiedActionOf) +} +``` + +And in the reducer, instead of invoking +``Reducer/forEach(_:action:element:fileID:filePath:line:column:)-6zye8`` with a case path using the +`/` prefix operator: + +```swift +Reduce { state, action in + // ... +} +.forEach(\.rows, action: /Action.row(id:action:)) { + RowFeature() +} +``` + +…you will instead use key path syntax to determine which case of the `Action` enum holds the +identified action: + +```swift +Reduce { state, action in + // ... +} +.forEach(\.rows, action: \.rows) { + RowFeature() +} +``` + +This syntax is shorter, more familiar, and can better leverage Xcode autocomplete and +type-inference. + +One last change you will need to make is anywhere you are destructuring the old-style action you +will need to insert a `.element` layer: + +```diff +-case let .row(id: id, action: .buttonTapped): ++case let .rows(.element(id: id, action: .buttonTapped)): +``` + +[swift-case-paths]: http://github.com/pointfreeco/swift-case-paths +[tca-discussions]: http://github.com/pointfreeco/swift-composable-architecture/discussions diff --git a/README.md b/README.md index 2d7827b4..b99ca327 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ library, [subscribe today](https://www.pointfree.co/pricing). ## Overview SharingGRDB is a [fast](#performance), lightweight replacement for SwiftData that deploys all the -way back to the iOS 13 generation of targets. +way back to the iOS 13 generation of targets. To populate data from the database you can use +the `@FetchAll` property wrapper, which is similar to SwiftData's `@Query` macro: @@ -45,8 +46,8 @@ way back to the iOS 13 generation of targets.
```swift -@SharedReader(.fetchAll(Item.all)) -var items +@FetchAll +var items: [Item] @Table struct Item { @@ -153,11 +154,14 @@ struct MyApp: App { > [Preparing a SQLite database][preparing-db-article]. This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies, like -[`fetchAll`][fetchall-docs]: +[`@FetchAll`][fetchall-docs] and [`@FetchOne`][fetchone-docs]: ```swift -@SharedReader(.fetchAll(Item.all)) +@FetchAll var items: [Item] + +@FetchOne(Item.where(\.isInStock).count()) +var inStockItemsCount = 0 ``` And you can access this database throughout your application in a way similar to how one accesses @@ -216,8 +220,11 @@ the [articles][articles] below to learn how to best utilize this library: [articles]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb#Essentials [comparison-swiftdata-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/comparisonwithswiftdata [fetching-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/fetching -[preparing-db-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/preparingdatabase +[preparing-db-article]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/preparingdatabase + + [fetchall-docs]: https://swiftpackageindex.com/pointfreeco/sharing-grdb/main/documentation/sharinggrdb/sharing/sharedreaderkey/fetchall(sql:arguments:database:animation:) +[fetchone-docs]: todo: find link ## Performance diff --git a/SharingGRDB.xcworkspace/contents.xcworkspacedata b/SharingGRDB.xcworkspace/contents.xcworkspacedata index b50680ee..6f7a50bd 100644 --- a/SharingGRDB.xcworkspace/contents.xcworkspacedata +++ b/SharingGRDB.xcworkspace/contents.xcworkspacedata @@ -1,6 +1,9 @@ + + diff --git a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md index f74decfe..b95003f3 100644 --- a/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md +++ b/Sources/SharingGRDB/Documentation.docc/SharingGRDB.md @@ -13,7 +13,7 @@ which this module automatically exports. > [StructuredQueries][], consider depending on `SharingGRDBCore`, instead. See [`SharingGRDBCore`](sharinggrdbcore) for documentation on the integration with the -`@SharedReader` property wrapper, which is equivalent to SwiftData's `@Query`. +`@FetchAll` property wrapper, which is equivalent to SwiftData's `@Query`. See [`StructuredQueriesGRDBCore`](sharinggrdbcore) for documentation on the integration between [StructuredQueries][] and [GRDB][]. diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md index 3e344239..2c436dee 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -123,7 +123,7 @@ configure your SQLite database for use with SharingGRDB. ### Fetching data for a view -To fetch data from a SQLite database you use the `@SharedReader` property wrapper in SharingGRDB, +To fetch data from a SQLite database you use the `@FetchAll` property wrapper in SharingGRDB, whereas you use the `@Query` macro with SwiftData: @Row { @@ -131,7 +131,7 @@ whereas you use the `@Query` macro with SwiftData: ```swift // SharingGRDB struct ItemsView: View { - @SharedReader(.fetchAll(Item.order(by: \.title))) + @FetchAll(Item.order(by: \.title)) var items var body: some View { @@ -159,16 +159,30 @@ whereas you use the `@Query` macro with SwiftData: } } -The `@SharedReader` property wrapper takes a variety of options, detailed more in , -and allows you to write queries using a type-safe and schema-safe builder syntax, or you can write -safe SQL strings that are schema-safe and protect you from SQL injection. +The `@FetchAll` property wrapper takes a variety of options and allows you to write queries using a +type-safe and schema-safe builder syntax, or you can write safe SQL strings that are schema-safe and +protect you from SQL injection. + +The library also ships a few other property wrappres that have no equivalent in SwiftData. For +example, the [`@FetchOne`]() property wrapper allows you to query for just a single +value, which can be useful for computing aggegrate data: + +```swift +@FetchOne(Item.where(\.isInStock).count()) +var inStockItemsCount = 0 +``` + +And the [`@Fetch`]() property wrapper allows you to execute multiple queries in a single +database transaction to gather you data into a single data type. SwiftData has no equivalent for +either of these operations. See for more detailed information on how to fetch +data from your database using the tools of this library. ### Fetching data for an @Observable model There are many reasons one may want to move logic out of the view and into an `@Observable` model, such as allowing to unit test your feature's logic, and making it possible to deep link in your -app. The `@SharedReader` and [data fetching tools]() work just as well in an -`@Observable` model as they do in a SwiftUI view. The state held in the property wrapper +app. The `@FetchAll` property warpper, and other [data fetching tools]() work just as +well in an `@Observable` model as they do in a SwiftUI view. The state held in the property wrapper automatically updates when changes are made to the database. The `@Query` macro, on the other hand, only works in SwiftUI views. This means if you want to move @@ -182,7 +196,7 @@ its functionality from scratch: @Observable class FeatureModel { @ObservationIgnored - @SharedReader(.fetchAll(Item.order(by: \.title)) + @FetchAll(Item.order(by: \.title)) var items // ... } ``` @@ -227,8 +241,8 @@ its functionality from scratch: } } -> Note: It is necessary to annotate `@SharedReader` with `@ObservationIgnored` when using the -> `@Observable` macro due to how macros interact with property wrappers. However, `@SharedReader` +> Note: It is necessary to annotate `@FetchAll` with `@ObservationIgnored` when using the +> `@Observable` macro due to how macros interact with property wrappers. However, `@FetchAll` > handles its own observation, and so state will still be observed when accessed in a view. ### Dynamic queries @@ -243,7 +257,7 @@ search for rows in a table: // SharingGRDB struct ItemsView: View { @State var searchText = "" - @SharedReader(value: []) var items: [Item] + @FetchAll var items: [Item] var body: some View { ForEach(items) { item in @@ -310,9 +324,9 @@ because `@Query` state is not mutable after it is initialized. The only way to c state is if the view holding it is reinitialized, which requires a parent view to recreate the child view. -On the other hand, the same UI made with `@SharedReader` can all happen in a single view. We can +On the other hand, the same UI made with `@FetchAll` can all happen in a single view. We can hold onto the `searchText` state that the user edits, use the `searchable` view modifier for the -UI, and update the `@SharedReader` query when the `searchText` state changes. +UI, and update the `@FetchAll` query when the `searchText` state changes. See for more information on how to execute dynamic queries in the library. @@ -463,15 +477,13 @@ struct SportWithTeamCount { let teamCount: Int } -@SharedReader( - .fetchAll( - Sport - .group(by: \.id) - .leftJoin(Team.all) { $0.id.eq($1.sportID) } - .select { - SportWithTeamCount.Columns(sport: $0, teamCount: $1.count()) - } - ) +@FetchAll( + Sport + .group(by: \.id) + .leftJoin(Team.all) { $0.id.eq($1.sportID) } + .select { + SportWithTeamCount.Columns(sport: $0, teamCount: $1.count()) + } ) var sportsWithTeamCounts ``` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/DynamicQueries.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/DynamicQueries.md index de3f62ff..11bf5760 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/DynamicQueries.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/DynamicQueries.md @@ -17,7 +17,7 @@ Take the following example: ```swift struct ContentView: View { - @SharedReader(.fetchAll(Item.all) var items + @FetchAll var items: [Item] @State var filterDate: Date? @State var order: SortOrder = .reverse @@ -88,9 +88,9 @@ struct ContentView: View { ``` > Important: If a parent view refreshes, a dynamically-updated query can be overwritten with the -> initial `@SharedReader`'s value, taken from the parent. To manage the state of this dynamic query -> locally to this view, we use `@State.SharedReader`, instead, which wraps a `@SharedReader` in -> SwiftUI `@State`. +> initial `@FetchAll`'s value, taken from the parent. To manage the state of this dynamic query +> locally to this view, we use `@State @FetchAll`, instead, and to access the underlying +> `FetchAll` value you can use `wrappedValue`. > Note: We are using the ``Sharing/SharedReaderKey/fetchAll(_:database:)`` style of > querying the database. See for more APIs that can be used. diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md index e66a156f..59cddf1a 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md @@ -4,8 +4,23 @@ Learn about the various tools for fetching data from a SQLite database. ## Overview -All data fetching happens by providing the `fetchAll`, `fetchOne`, or `fetch` key to the -`@SharedReader` property wrapper. The primary differences between these choices is whether you want +All data fetching happens by using the `@FetchAll`, `@FetchOne` or `@Fetch` property wrappers. +The primary difference between these choices is whether if you want to fetch a collection of +rows, or fetch a single row (e.g. an aggegrate computation), or if you want to execute multiple +queries in a single transaction. + +* [@FetchAll](#) +* [@FetchOne](#) +* [@Fetch](#) + + +### FetchAll + + +---- + + +The primary differences between these choices is whether you want to build queries with [StructuredQueries][structured-queries-gh], specify your query as a raw SQL string, or if you want to assemble your value from one or more queries using a raw database connection. diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides.md new file mode 100644 index 00000000..38e793af --- /dev/null +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides.md @@ -0,0 +1,17 @@ +# Migration guides + +Learn how to upgrade your application to the newest version of SharingGRDB. + +## Overview + +SharingGRDB is under constant development, and we are always looking for ways to +simplify the library, and make it more powerful. As such, we often need to deprecate certain APIs +in favor of newer ones. We recommend people update their code as quickly as possible to the newest +APIs, and these guides contain tips to do so. + +> Important: Before following any particular migration guide be sure you have followed all the +> preceding migration guides. + +## Topics + +- diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md new file mode 100644 index 00000000..f6098da5 --- /dev/null +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md @@ -0,0 +1,21 @@ +# Migrating to 1.4 + +Update your code to make use of powerful new querying capabilities. + +## Overview + +SharingGRDB is under constant development, and we are always looking for ways to +simplify the library, and make it more powerful. As such, we often need to deprecate certain APIs +in favor of newer ones. We recommend people update their code as quickly as possible to the newest +APIs, and these guides contain tips to do so. + +> Important: Before following any particular migration guide be sure you have followed all the +> preceding migration guides. + +* [FetchAll, FetchOne, Fetch](#) +* [fetchAll, fetchOne, fetch: soft-deprecated](#) +* [Avoiding the cost of macros](#) + + From c019dd6f4b305fb8e5e2cd5aeaf52c0e22782542 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 17:27:02 -0700 Subject: [PATCH 157/171] wip --- MigrationGuide0.2.md | 318 ------------------ .../contents.xcworkspacedata | 3 - 2 files changed, 321 deletions(-) delete mode 100644 MigrationGuide0.2.md diff --git a/MigrationGuide0.2.md b/MigrationGuide0.2.md deleted file mode 100644 index acb19f7d..00000000 --- a/MigrationGuide0.2.md +++ /dev/null @@ -1,318 +0,0 @@ -# Migrating to 1.4 - -Update your code to make use of the ``Reducer()`` macro, and learn how to better leverage case key -paths in your features. - -## Overview - -The Composable Architecture is under constant development, and we are always looking for ways to -simplify the library, and make it more powerful. As such, we often need to deprecate certain APIs -in favor of newer ones. We recommend people update their code as quickly as possible to the newest -APIs, and this article contains some tips for doing so. - -* [Using the @Reducer macro](#Using-the-Reducer-macro) -* [Using case key paths](#Using-case-key-paths) -* [Receiving test store actions](#Receiving-test-store-actions) -* [Moving off of `TaskResult`](#Moving-off-of-TaskResult) -* [Identified actions](#Identified-actions) - -### Using the @Reducer macro - -Version 1.4 of the library has introduced a new macro for automating certain aspects of implementing -a ``Reducer``. It is called ``Reducer()``, and to migrate existing code one only needs to annotate -their type with `@Reducer`: - -```diff -+@Reducer - struct MyFeature: Reducer { - // ... - } -``` - -No other changes to be made, and you can immediately start taking advantage of new capabilities of -reducer composition, such as case key paths (see guides below). See the documentation of -``Reducer()`` to see everything that macro adds to your feature's reducer. - -You can also technically drop the ``Reducer`` conformance: - -```diff - @Reducer --struct MyFeature: Reducer { -+struct MyFeature { - // ... - } -``` - -However, there are some known issues in Xcode that cause autocomplete and type inference to break. -See the documentation of for more gotchas on using the `@Reducer` macro. - - -### Using case key paths - -In version 1.4 we soft-deprecated many APIs that take the `CasePath` type in favor of APIs that take -what is known as a `CaseKeyPath`. Both of these types come from our [CasePaths][swift-case-paths] -library and aim to allow one to abstract over the shape of enums just as key paths allow one to do -so with structs. - -However, in conjunction with version 1.4 of this library we also released an update to CasePaths -that massively improved the ergonomics of using case paths. We introduced the `@CasePathable` macro -for automatically deriving case paths so that we could stop using runtime reflection, and we -introduced a way of using key paths to describe case paths. And so the old `CasePath` type has been -deprecated, and the new `CaseKeyPath` type has taken its place. - -This means that previously when you would use APIs involving case paths you would have to use the -`/` prefix operator to derive the case path. For example: - -```swift -Reduce { state, action in - // ... -} -.ifLet(\.child, action: /Action.child) { - ChildFeature() -} -``` - -You now get to shorten that into a far simpler, more familiar key path syntax: - -```swift -Reduce { state, action in - // ... -} -.ifLet(\.child, action: \.child) { - ChildFeature() -} -``` - -To be able to take advantage of this syntax with your feature's actions, you must annotate your -``Reducer`` conformances with the ``Reducer()`` macro: - -```swift -@Reducer -struct Feature { - // ... -} -``` - -Which automatically applies the `@CasePathable` macro to the feature's `Action` enum among other -things: - -```diff -+@CasePathable - enum Action { - // ... - } -``` - -Further, if the feature's `State` is an enum, `@CasePathable` will also be applied, along with -`@dynamicMemberLookup`: - -```diff -+@CasePathable -+@dynamicMemberLookup - enum State { - // ... - } -``` - -Dynamic member lookups allows a state's associated value to be accessed via dot-syntax, which can be -useful when scoping a store's state to a specific case: - -```diff - IfLetStore( - store.scope( -- state: /Feature.State.tray, action: Feature.Action.tray -+ state: \.tray, action: { .tray($0) } - ) -) { store in - // ... -} -``` - -To form a case key path for any other enum, you must apply the `@CasePathable` macro explicitly: - -```swift -@CasePathable -enum DelegateAction { - case didFinish(success: Bool) -} -``` - -And to access its associated values, you must also apply the `@dynamicMemberLookup` attributes: - -```swift -@CasePathable -@dynamicMemberLookup -enum DestinationState { - case tray(Tray.State) -} -``` - -Anywhere you previously used the `/` prefix operator for case paths you should now be able to use -key path syntax, so long as all of the enums involved are `@CasePathable`. - -If you encounter any problems, create a [discussion][tca-discussions] on the Composable Architecture -repo. - -### Receiving test store actions - -The power of case key paths and the `@CasePathable` macro has made it possible to massively simplify -how one asserts on actions received in a ``TestStore``. Instead of constructing the concrete action -received from an effect like this: - -```swift -store.receive(.child(.presented(.response(.success("Hello!"))))) -``` - -…you can use key path syntax to describe the nesting of action cases that is received: - -```swift -store.receive(\.child.presented.response.success) -``` - -> Note: Case key path syntax requires that every nested action is `@CasePathable`. Reducer actions -> are typically `@CasePathable` automatically via the ``Reducer()`` macro, but other enums must be -> explicitly annotated: -> -> ```swift -> @CasePathable -> enum DelegateAction { -> case didFinish(success: Bool) -> } -> ``` - -And in the case of ``PresentationAction`` you can even omit the ``PresentationAction/presented(_:)`` -path component: - -```swift -store.receive(\.child.response.success) -``` - -This does not assert on the _data_ received in the action, but typically that is already covered -by the state assertion made inside the trailing closure of `receive`. And if you use this style of -action receiving exclusively, you can even stop conforming your action types to `Equatable`. - -There are a few advanced situations to be aware of. When receiving an action that involves an -``IdentifiedAction`` (more information below in ), then -you can use the subscript ``IdentifiedAction/AllCasePaths-swift.struct/subscript(id:)`` to -receive a particular action for an element: - -```swift -store.receive(\.rows[id: 0].response.success) -``` - -And the same goes for ``StackAction`` too: - -```swift -store.receive(\.path[id: 0].response.success) -``` - -### Moving off of TaskResult - -In version 1.4 of the library, the ``TaskResult`` was soft-deprecated and eventually will be fully -deprecated and then removed. The original rationale for the introduction of ``TaskResult`` was to -make an equatable-friendly version of `Result` for when the error produced was `any Error`, which is -not equatable. And the reason to want an equatable-friendly result is so that the `Action` type in -reducers can be equatable, and the reason for _that_ is to make it possible to test actions -emitted by effects. - -Typically in tests, when one wants to assert that the ``TestStore`` received an action you must -specify a concrete action: - -```swift -store.receive(.response(.success("Hello!"))) { - // ... -} -``` - -The ``TestStore`` uses the equatable conformance of `Action` to confirm that you are asserting that -the store received the correct action. - -However, this becomes verbose when testing deeply nested features, which is common in integration -tests: - -```swift -store.receive(.child(.response(.success("Hello!")))) { - // ... -} -``` - -However, with the introduction of [case key paths][swift-case-paths] we greatly improved the -ergonomics of referring to deeply nested enums. You can now use key path syntax to describe the -case of the enum you expect to receive, and you can even omit the associated data from the action -since typically that is covered in the state assertion: - -```swift -store.receive(\.child.response.success) { - // ... -} -``` - -And this syntax does not require the `Action` enum to be equatable since we are only asserting that -the case of the action was received. We are not testing the data in the action. - -We feel that with this better syntax there is less of a reason to have ``TaskResult`` and so we -do plan on removing it eventually. If you have an important use case for ``TaskResult`` that you -think merits it being in the library, please [open a discussion][tca-discussions]. - -### Identified actions - -In version 1.4 of the library we introduced the ``IdentifiedAction`` type which makes it more -ergonomic to bundle the data needed for actions in collections of data. Previously you would -have a case in your `Action` enum for a particular row that holds the ID of the state being acted -upon as well as the action: - -```swift -enum Action { - // ... - case row(id: State.ID, action: Action) -} -``` - -This can be updated to hold onto ``IdentifiedAction`` instead of those piece of data directly in the -case: - -```swift -enum Action { - // ... - case rows(IdentifiedActionOf) -} -``` - -And in the reducer, instead of invoking -``Reducer/forEach(_:action:element:fileID:filePath:line:column:)-6zye8`` with a case path using the -`/` prefix operator: - -```swift -Reduce { state, action in - // ... -} -.forEach(\.rows, action: /Action.row(id:action:)) { - RowFeature() -} -``` - -…you will instead use key path syntax to determine which case of the `Action` enum holds the -identified action: - -```swift -Reduce { state, action in - // ... -} -.forEach(\.rows, action: \.rows) { - RowFeature() -} -``` - -This syntax is shorter, more familiar, and can better leverage Xcode autocomplete and -type-inference. - -One last change you will need to make is anywhere you are destructuring the old-style action you -will need to insert a `.element` layer: - -```diff --case let .row(id: id, action: .buttonTapped): -+case let .rows(.element(id: id, action: .buttonTapped)): -``` - -[swift-case-paths]: http://github.com/pointfreeco/swift-case-paths -[tca-discussions]: http://github.com/pointfreeco/swift-composable-architecture/discussions diff --git a/SharingGRDB.xcworkspace/contents.xcworkspacedata b/SharingGRDB.xcworkspace/contents.xcworkspacedata index 6f7a50bd..b50680ee 100644 --- a/SharingGRDB.xcworkspace/contents.xcworkspacedata +++ b/SharingGRDB.xcworkspace/contents.xcworkspacedata @@ -1,9 +1,6 @@ - - From a5e558a72924d5f3da686f49d869d5503cd8b034 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 17:49:44 -0700 Subject: [PATCH 158/171] wip --- Package.resolved | 6 ++-- .../Articles/Deprecations.md | 14 ++++++++++ .../MigrationGuides/MigratingTo0.2.md | 2 +- .../Documentation.docc/Extensions/Fetch.md | 4 +-- .../Documentation.docc/Extensions/FetchAll.md | 27 ++++++++++++++++-- .../Documentation.docc/Extensions/FetchOne.md | 28 +++++++++++++++++-- .../Documentation.docc/SharingGRDBCore.md | 18 +++++------- 7 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 Sources/SharingGRDBCore/Documentation.docc/Articles/Deprecations.md diff --git a/Package.resolved b/Package.resolved index 3514feb3..530ddfcc 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ccc2c1d01b329a42bbfac790e28391eb1352919add9af7ad0dce67fa9f5ef18a", + "originHash" : "a8477db1fe79838ddca336ba53399bdd7e6a459f15e01079fc2acdc8b44a6077", "pins" : [ { "identity" : "combine-schedulers", @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "branch" : "main", - "revision" : "eaf60f59cac4074abee164048ada8e4920789e26" + "revision" : "7375bc75c4acaedffee9923e496b93fab18a7bd7", + "version" : "0.1.0" } }, { diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/Deprecations.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/Deprecations.md new file mode 100644 index 00000000..b2eb37c8 --- /dev/null +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/Deprecations.md @@ -0,0 +1,14 @@ +# Deprecations + +Review unsupported APIs and their replacements. + +## Overview + +Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use +instead. + +## Topics + +### Sharing extensions + +- ``Sharing`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md index f6098da5..d835e941 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md @@ -1,4 +1,4 @@ -# Migrating to 1.4 +# Migrating to 0.2 Update your code to make use of powerful new querying capabilities. diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md index 6fbf85ba..5fede086 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md @@ -1,4 +1,4 @@ -# ``Sharing/SharedReaderKey/fetch(_:database:)`` +# ``SharingGRDBCore/Fetch`` ## Overview @@ -10,7 +10,7 @@ ### SwiftUI integration -- ``Sharing/SharedReaderKey/fetch(_:database:animation:)`` +- ``init(_:database:animation:)`` ### Custom scheduling diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md index 9fdd7ac6..f594c271 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md @@ -1,13 +1,34 @@ -# ``Sharing/SharedReaderKey/fetchAll(_:database:)`` +# ``SharingGRDBCore/FetchAll`` ## Overview ## Topics +### Fetching data + +- ``init(database:)`` +- ``init(_:database:)`` +- ``load(_:database:)`` + +### Accessing state + +- ``wrappedValue`` +- ``projectedValue`` +- ``isLoading`` +- ``loadError`` + ### SwiftUI integration -- ``Sharing/SharedReaderKey/fetchAll(_:database:animation:)`` +- ``init(database:animation:)`` +- ``init(_:database:animation:)`` +- ``load(_:database:animation:)`` + +### Combine integration + +- ``publisher`` ### Custom scheduling -- ``Sharing/SharedReaderKey/fetchAll(_:database:scheduler:)`` +- ``init(database:scheduler:)`` +- ``init(_:database:scheduler:)`` +- ``load(_:database:scheduler:)`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md index 5c4f7307..66046ba9 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md @@ -1,13 +1,35 @@ -# ``Sharing/SharedReaderKey/fetchOne(_:database:)`` +# ``SharingGRDBCore/FetchOne`` ## Overview ## Topics +### Fetching data + +- ``init(wrappedValue:database:)`` +- ``init(database:)`` +- ``init(wrappedValue:_:database:)`` +- ``load(_:database:)`` + +### Accessing state + +- ``wrappedValue`` +- ``projectedValue`` +- ``isLoading`` +- ``loadError`` + ### SwiftUI integration -- ``Sharing/SharedReaderKey/fetchOne(_:database:animation:)`` +- ``init(wrappedValue:database:animation:)`` +- ``init(wrappedValue:_:database:animation:)`` +- ``load(_:database:animation:)`` + +### Combine integration + +- ``publisher`` ### Custom scheduling -- ``Sharing/SharedReaderKey/fetchOne(_:database:scheduler:)`` +- ``init(wrappedValue:database:scheduler:)`` +- ``init(wrappedValue:_:database:scheduler:)`` +- ``load(_:database:scheduler:)`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md index 5fea1c28..76ec02c5 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md +++ b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md @@ -229,22 +229,18 @@ with SQLite to take full advantage of GRDB and SharingGRDB. - - - +- ### Database configuration and access - ``Dependencies/DependencyValues/defaultDatabase`` -### Fetch strategies +### Fetch and observing queries -- ``Sharing/SharedReaderKey/fetchAll(_:database:)`` -- ``Sharing/SharedReaderKey/fetchOne(_:database:)`` -- ``Sharing/SharedReaderKey/fetch(_:database:)`` +- ``FetchAll`` +- ``FetchOne`` +- ``Fetch`` -### Raw SQL strategies +### Deprecated interfaces -- ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` -- ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:)`` - -### Custom requests - -- ``FetchKeyRequest`` +- From 9482b6881c8c78e26904886f8a18adff5c79cc05 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 17:52:37 -0700 Subject: [PATCH 159/171] wip --- .../Extensions/FetchAllSQL.md | 13 -------- .../Extensions/FetchOneSQL.md | 13 -------- Sources/SharingGRDBCore/Fetch.swift | 33 ++++++++++++------- Sources/SharingGRDBCore/FetchAll.swift | 2 ++ Sources/SharingGRDBCore/FetchOne.swift | 2 ++ 5 files changed, 25 insertions(+), 38 deletions(-) delete mode 100644 Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAllSQL.md delete mode 100644 Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOneSQL.md diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAllSQL.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAllSQL.md deleted file mode 100644 index 93aa47da..00000000 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAllSQL.md +++ /dev/null @@ -1,13 +0,0 @@ -# ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:)`` - -## Overview - -## Topics - -### SwiftUI integration - -- ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:animation:)`` - -### Custom scheduling - -- ``Sharing/SharedReaderKey/fetchAll(sql:arguments:database:scheduler:)`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOneSQL.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOneSQL.md deleted file mode 100644 index e20a0b6d..00000000 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOneSQL.md +++ /dev/null @@ -1,13 +0,0 @@ -# ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:)`` - -## Overview - -## Topics - -### SwiftUI integration - -- ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:animation:)`` - -### Custom scheduling - -- ``Sharing/SharedReaderKey/fetchOne(sql:arguments:database:scheduler:)`` diff --git a/Sources/SharingGRDBCore/Fetch.swift b/Sources/SharingGRDBCore/Fetch.swift index f76abab9..a089760e 100644 --- a/Sources/SharingGRDBCore/Fetch.swift +++ b/Sources/SharingGRDBCore/Fetch.swift @@ -5,12 +5,21 @@ import SwiftUI #endif +/// A property that can query for data in a SQLite database. +/// +/// It takes a ``FetchKeyRequest`` that describes how to fetch data from a database: +/// +/// ```swift +/// @Fetch(Items()) var items = Items.Value() +/// ``` +/// +/// See for more information. @propertyWrapper public struct Fetch: Sendable { - public var _sharedReader: SharedReader + private var sharedReader: SharedReader public var wrappedValue: Value { - _sharedReader.wrappedValue + sharedReader.wrappedValue } public var projectedValue: Self { @@ -18,21 +27,21 @@ public struct Fetch: Sendable { } public var loadError: (any Error)? { - _sharedReader.loadError + sharedReader.loadError } public var isLoading: Bool { - _sharedReader.isLoading + sharedReader.isLoading } #if canImport(Combine) public var publisher: some Publisher { - _sharedReader.publisher + sharedReader.publisher } #endif public init(wrappedValue: sending Value) { - _sharedReader = SharedReader(value: wrappedValue) + sharedReader = SharedReader(value: wrappedValue) } public init( @@ -40,14 +49,14 @@ public struct Fetch: Sendable { _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil ) { - _sharedReader = SharedReader(wrappedValue: wrappedValue, .fetch(request, database: database)) + sharedReader = SharedReader(wrappedValue: wrappedValue, .fetch(request, database: database)) } public init( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil ) where Value: RangeReplaceableCollection { - _sharedReader = SharedReader(.fetch(request, database: database)) + sharedReader = SharedReader(.fetch(request, database: database)) } } @@ -58,7 +67,7 @@ extension Fetch { database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable ) { - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch(request, database: database, scheduler: scheduler) ) @@ -69,7 +78,7 @@ extension Fetch { database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable ) where Value: RangeReplaceableCollection { - _sharedReader = SharedReader(.fetch(request, database: database, scheduler: scheduler)) + sharedReader = SharedReader(.fetch(request, database: database, scheduler: scheduler)) } } @@ -81,7 +90,7 @@ extension Fetch { database: (any DatabaseReader)? = nil, animation: Animation ) { - _sharedReader = SharedReader( + sharedReader = SharedReader( wrappedValue: wrappedValue, .fetch(request, database: database, animation: animation) ) @@ -92,7 +101,7 @@ extension Fetch { database: (any DatabaseReader)? = nil, animation: Animation ) where Value: RangeReplaceableCollection { - _sharedReader = SharedReader(.fetch(request, database: database, animation: animation)) + sharedReader = SharedReader(.fetch(request, database: database, animation: animation)) } } #endif diff --git a/Sources/SharingGRDBCore/FetchAll.swift b/Sources/SharingGRDBCore/FetchAll.swift index 3733665b..f767dea2 100644 --- a/Sources/SharingGRDBCore/FetchAll.swift +++ b/Sources/SharingGRDBCore/FetchAll.swift @@ -12,6 +12,8 @@ /// ```swift /// @FetchAll(Item.order(by: \.name)) var items /// ``` +/// +/// See for more information. @propertyWrapper public struct FetchAll: Sendable { private var sharedReader: SharedReader<[Element]> = SharedReader(value: []) diff --git a/Sources/SharingGRDBCore/FetchOne.swift b/Sources/SharingGRDBCore/FetchOne.swift index f83df9d6..ffd950f3 100644 --- a/Sources/SharingGRDBCore/FetchOne.swift +++ b/Sources/SharingGRDBCore/FetchOne.swift @@ -12,6 +12,8 @@ /// ```swift /// @FetchOne(Item.count) var itemsCount = 0 /// ``` +/// +/// See for more information. @propertyWrapper public struct FetchOne: Sendable { private var sharedReader: SharedReader From e7d9ef822cf353ac1b732e39e58da4e16a63d469 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 18:06:52 -0700 Subject: [PATCH 160/171] wip --- .../Documentation.docc/Extensions/Fetch.md | 24 +++++- Sources/SharingGRDBCore/Fetch.swift | 76 ++++++++++++++++--- Sources/SharingGRDBCore/FetchKey.swift | 32 ++++++++ 3 files changed, 120 insertions(+), 12 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md index 5fede086..76d62de0 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md @@ -4,17 +4,35 @@ ## Topics -### Custom queries +### Fetching data - ``FetchKeyRequest`` +- ``init(wrappedValue:_:database:)`` +- ``init(_:database:)`` +- ``init(database:)`` +- ``init(wrappedValue:)`` +- ``load(_:database:)`` + +### Accessing state + +- ``wrappedValue`` +- ``projectedValue`` +- ``isLoading`` +- ``loadError`` ### SwiftUI integration -- ``init(_:database:animation:)`` +- ``init(wrappedValue:_:database:animation:)`` +- ``load(_:database:animation:)`` + +### Combine integration + +- ``publisher`` ### Custom scheduling -- ``Sharing/SharedReaderKey/fetch(_:database:scheduler:)`` +- ``init(wrappedValue:_:database:scheduler:)`` +- ``load(_:database:scheduler:)`` ### Sharing infrastructure diff --git a/Sources/SharingGRDBCore/Fetch.swift b/Sources/SharingGRDBCore/Fetch.swift index a089760e..e473aa33 100644 --- a/Sources/SharingGRDBCore/Fetch.swift +++ b/Sources/SharingGRDBCore/Fetch.swift @@ -18,32 +18,50 @@ public struct Fetch: Sendable { private var sharedReader: SharedReader + /// Data associated with the underlying query. public var wrappedValue: Value { sharedReader.wrappedValue } + /// Returns this property wrapper. + /// + /// Useful if you want to access various property wrapper state, like ``loadError``, + /// ``isLoading``, and ``publisher``. public var projectedValue: Self { self } + /// An error encountered during the most recent attempt to load data. public var loadError: (any Error)? { sharedReader.loadError } + /// Whether or not data is loading from the database. public var isLoading: Bool { sharedReader.isLoading } #if canImport(Combine) + /// A publisher that emits events when the database observes changes to the query. public var publisher: some Publisher { sharedReader.publisher } #endif + /// Initializes this property with an initial value. + /// + /// - Parameter wrappedValue: A default value to associate with this property. public init(wrappedValue: sending Value) { sharedReader = SharedReader(value: wrappedValue) } + /// Initializes this property with a request associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - request: A request describing the data to fetch. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). public init( wrappedValue: Value, _ request: some FetchKeyRequest, @@ -52,15 +70,30 @@ public struct Fetch: Sendable { sharedReader = SharedReader(wrappedValue: wrappedValue, .fetch(request, database: database)) } - public init( + /// Replaces the wrapped value with data from the given request. + /// + /// - Parameters: + /// - request: A request describing the data to fetch. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + public func load( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil - ) where Value: RangeReplaceableCollection { - sharedReader = SharedReader(.fetch(request, database: database)) + ) async throws { + try await sharedReader.load(.fetch(request, database: database)) } } extension Fetch { + /// Initializes this property with a request associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - request: A request describing the data to fetch. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public init( wrappedValue: Value, _ request: some FetchKeyRequest, @@ -73,17 +106,34 @@ extension Fetch { ) } - public init( + /// Replaces the wrapped value with data from the given request. + /// + /// - Parameters: + /// - request: A request describing the data to fetch. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + public func load( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable - ) where Value: RangeReplaceableCollection { - sharedReader = SharedReader(.fetch(request, database: database, scheduler: scheduler)) + ) async throws { + try await sharedReader.load(.fetch(request, database: database, scheduler: scheduler)) } } #if canImport(SwiftUI) extension Fetch { + /// Initializes this property with a request associated with the wrapped value. + /// + /// - Parameters: + /// - wrappedValue: A default value to associate with this property. + /// - request: A request describing the data to fetch. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. public init( wrappedValue: Value, _ request: some FetchKeyRequest, @@ -96,12 +146,20 @@ extension Fetch { ) } - public init( + /// Replaces the wrapped value with data from the given request. + /// + /// - Parameters: + /// - request: A request describing the data to fetch. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + public func load( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil, animation: Animation - ) where Value: RangeReplaceableCollection { - sharedReader = SharedReader(.fetch(request, database: database, animation: animation)) + ) async throws { + try await sharedReader.load(.fetch(request, database: database, animation: animation)) } } #endif diff --git a/Sources/SharingGRDBCore/FetchKey.swift b/Sources/SharingGRDBCore/FetchKey.swift index 5438c365..041b2a32 100644 --- a/Sources/SharingGRDBCore/FetchKey.swift +++ b/Sources/SharingGRDBCore/FetchKey.swift @@ -45,6 +45,10 @@ extension SharedReaderKey { /// - database: The database to read from. A value of `nil` will use /// `@Dependency(\.defaultDatabase)``. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(macOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(tvOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(watchOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") public static func fetch( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil @@ -68,6 +72,10 @@ extension SharedReaderKey { /// - database: The database to read from. A value of `nil` will use /// `@Dependency(\.defaultDatabase)``. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(macOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(tvOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(watchOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") public static func fetch( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil @@ -93,6 +101,10 @@ extension SharedReaderKey { /// - database: The database to read from. A value of `nil` will use /// `@Dependency(\.defaultDatabase)``. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") + @available(macOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") + @available(tvOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") + @available(watchOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") public static func fetchAll( sql: String, arguments: StatementArguments = StatementArguments(), @@ -122,6 +134,10 @@ extension SharedReaderKey { /// - database: The database to read from. A value of `nil` will use /// `@Dependency(\.defaultDatabase)``. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") + @available(macOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") + @available(tvOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") + @available(watchOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") public static func fetchOne( sql: String, arguments: StatementArguments = StatementArguments(), @@ -146,6 +162,10 @@ extension SharedReaderKey { /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(macOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(tvOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(watchOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") public static func fetch( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil, @@ -168,6 +188,10 @@ extension SharedReaderKey { /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(macOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(tvOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(watchOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") public static func fetch( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil, @@ -191,6 +215,10 @@ extension SharedReaderKey { /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") + @available(macOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") + @available(tvOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") + @available(watchOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") public static func fetchAll( sql: String, arguments: StatementArguments = StatementArguments(), @@ -220,6 +248,10 @@ extension SharedReaderKey { /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") + @available(macOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") + @available(tvOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") + @available(watchOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") public static func fetchOne( sql: String, arguments: StatementArguments = StatementArguments(), From 029888528174e89051aa06f89d171d0a6e5b9b23 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 18:09:20 -0700 Subject: [PATCH 161/171] wip --- Examples/CaseStudies/DynamicQuery.swift | 8 ++++---- Sources/SharingGRDBCore/FetchKey+SwiftUI.swift | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Examples/CaseStudies/DynamicQuery.swift b/Examples/CaseStudies/DynamicQuery.swift index 9825a61c..dd2d5f40 100644 --- a/Examples/CaseStudies/DynamicQuery.swift +++ b/Examples/CaseStudies/DynamicQuery.swift @@ -8,12 +8,12 @@ struct DynamicQueryDemo: SwiftUICaseStudy { facts for text, and the list will stay in sync so that if a new fact is added to the database \ that satisfies the search term, it will immediately appear. - To accomplish this one can invoke the `load` method defined on the `@SharedReader` projected \ - value in order to set a new query with dynamic parameters. + To accomplish this one can invoke the `load` method defined on the `@Fetch` projected value in \ + order to set a new query with dynamic parameters. """ let caseStudyTitle = "Dynamic Query" - @State.SharedReader(.fetch(Facts(), animation: .default)) private var facts = Facts.Value() + @Fetch(Facts(), animation: .default) private var facts = Facts.Value() @State var query = "" @Dependency(\.defaultDatabase) var database @@ -53,7 +53,7 @@ struct DynamicQueryDemo: SwiftUICaseStudy { .searchable(text: $query) .task(id: query) { await withErrorReporting { - try await $facts.load(.fetch(Facts(query: query), animation: .default)) + try await $facts.load(Facts(query: query), animation: .default) } } .task { diff --git a/Sources/SharingGRDBCore/FetchKey+SwiftUI.swift b/Sources/SharingGRDBCore/FetchKey+SwiftUI.swift index 7b7ec499..4e161d30 100644 --- a/Sources/SharingGRDBCore/FetchKey+SwiftUI.swift +++ b/Sources/SharingGRDBCore/FetchKey+SwiftUI.swift @@ -14,6 +14,10 @@ /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(macOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(tvOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(watchOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") public static func fetch( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil, @@ -33,6 +37,10 @@ /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(macOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(tvOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") + @available(watchOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") public static func fetch( _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil, @@ -53,6 +61,10 @@ /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") + @available(macOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") + @available(tvOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") + @available(watchOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") public static func fetchAll( sql: String, arguments: StatementArguments = StatementArguments(), @@ -79,6 +91,10 @@ /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. + @available(iOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") + @available(macOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") + @available(tvOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") + @available(watchOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") public static func fetchOne( sql: String, arguments: StatementArguments = StatementArguments(), From 034f7ba29e1bd70eafcc7ea6556a9e553a31d42a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 18:10:11 -0700 Subject: [PATCH 162/171] wip --- Sources/SharingGRDBCore/FetchKeyRequest.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/SharingGRDBCore/FetchKeyRequest.swift b/Sources/SharingGRDBCore/FetchKeyRequest.swift index 5f74f56d..cd89d074 100644 --- a/Sources/SharingGRDBCore/FetchKeyRequest.swift +++ b/Sources/SharingGRDBCore/FetchKeyRequest.swift @@ -26,13 +26,12 @@ import GRDB /// } /// ``` /// -/// And then can be used with a `@SharedReader` and -/// ``Sharing/SharedReaderKey/fetch(_:database:animation:)`` to popular state in a SwiftUI view, +/// And then can be used with the ``Fetch`` property wrapper to popular state in a SwiftUI view, /// `@Observable` model, UIKit view controller, and more: /// /// ```swift /// struct PlayersView: View { -/// @SharedReader(.fetch(PlayersRequest())) var response +/// @Fetch(PlayersRequest()) var response /// /// var body: some View { /// ForEach(response.players) { player in From 5b8624d158e0c9635746498db5ad32ff8bb4040f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 18:27:39 -0700 Subject: [PATCH 163/171] wip --- .../Documentation.docc/Extensions/FetchAll.md | 12 +++---- Sources/SharingGRDBCore/FetchAll.swift | 33 +++++++++++++++++-- .../StructuredQueriesGRDBCore.md | 16 ++++----- Tests/SharingGRDBTests/IntegrationTests.swift | 6 ++-- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md index f594c271..526da121 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md @@ -6,8 +6,8 @@ ### Fetching data -- ``init(database:)`` -- ``init(_:database:)`` +- ``init(wrappedValue:database:)`` +- ``init(wrappedValue:_:database:)`` - ``load(_:database:)`` ### Accessing state @@ -19,8 +19,8 @@ ### SwiftUI integration -- ``init(database:animation:)`` -- ``init(_:database:animation:)`` +- ``init(wrappedValue:database:animation:)`` +- ``init(wrappedValue:_:database:animation:)`` - ``load(_:database:animation:)`` ### Combine integration @@ -29,6 +29,6 @@ ### Custom scheduling -- ``init(database:scheduler:)`` -- ``init(_:database:scheduler:)`` +- ``init(wrappedValue:database:scheduler:)`` +- ``init(wrappedValue:_:database:scheduler:)`` - ``load(_:database:scheduler:)`` diff --git a/Sources/SharingGRDBCore/FetchAll.swift b/Sources/SharingGRDBCore/FetchAll.swift index f767dea2..7aa4fc28 100644 --- a/Sources/SharingGRDBCore/FetchAll.swift +++ b/Sources/SharingGRDBCore/FetchAll.swift @@ -50,11 +50,12 @@ public struct FetchAll: Sendable { /// Initializes this property with a query that fetches every row from a table. public init( + wrappedValue: [Element] = [], database: (any DatabaseReader)? = nil ) where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { let statement = Element.all.selectStar().asSelect() - self.init(statement, database: database) + self.init(wrappedValue: wrappedValue, statement, database: database) } /// Initializes this property with a query associated with the wrapped value. @@ -64,6 +65,7 @@ public struct FetchAll: Sendable { /// - database: The database to read from. A value of `nil` will use the default database /// (`@Dependency(\.defaultDatabase)`). public init( + wrappedValue: [Element] = [], _ statement: S, database: (any DatabaseReader)? = nil ) @@ -75,6 +77,7 @@ public struct FetchAll: Sendable { { let statement = statement.selectStar().asSelect() sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementValueRequest(statement: statement), database: database @@ -91,6 +94,7 @@ public struct FetchAll: Sendable { @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( + wrappedValue: [Element] = [], _ statement: S, database: (any DatabaseReader)? = nil ) @@ -103,6 +107,7 @@ public struct FetchAll: Sendable { { let statement = statement.selectStar().asSelect() sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch(FetchAllStatementPackRequest(statement: statement), database: database) ) } @@ -114,6 +119,7 @@ public struct FetchAll: Sendable { /// - database: The database to read from. A value of `nil` will use the default database /// (`@Dependency(\.defaultDatabase)`). public init( + wrappedValue: [Element] = [], _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil ) @@ -122,6 +128,7 @@ public struct FetchAll: Sendable { V.QueryOutput: Sendable { sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementValueRequest(statement: statement), database: database @@ -138,6 +145,7 @@ public struct FetchAll: Sendable { @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( + wrappedValue: [Element] = [], _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, database: (any DatabaseReader)? = nil ) @@ -147,6 +155,7 @@ public struct FetchAll: Sendable { repeat (each V2).QueryOutput: Sendable { sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementPackRequest(statement: statement), database: database @@ -264,12 +273,13 @@ extension FetchAll { /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. public init( + wrappedValue: [Element] = [], database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable ) where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { let statement = Element.all.selectStar().asSelect() - self.init(statement, database: database, scheduler: scheduler) + self.init(wrappedValue: wrappedValue, statement, database: database, scheduler: scheduler) } /// Initializes this property with a query associated with the wrapped value. @@ -281,6 +291,7 @@ extension FetchAll { /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. public init( + wrappedValue: [Element] = [], _ statement: S, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable @@ -293,6 +304,7 @@ extension FetchAll { { let statement = statement.selectStar().asSelect() sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -312,6 +324,7 @@ extension FetchAll { @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( + wrappedValue: [Element] = [], _ statement: S, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable @@ -325,6 +338,7 @@ extension FetchAll { { let statement = statement.selectStar().asSelect() sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementPackRequest(statement: statement), database: database, @@ -342,6 +356,7 @@ extension FetchAll { /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. public init( + wrappedValue: [Element] = [], _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable @@ -351,6 +366,7 @@ extension FetchAll { V.QueryOutput: Sendable { sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -370,6 +386,7 @@ extension FetchAll { @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( + wrappedValue: [Element] = [], _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable @@ -380,6 +397,7 @@ extension FetchAll { repeat (each V2).QueryOutput: Sendable { sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementPackRequest(statement: statement), database: database, @@ -514,12 +532,13 @@ extension FetchAll { /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. public init( + wrappedValue: [Element] = [], database: (any DatabaseReader)? = nil, animation: Animation ) where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { let statement = Element.all.selectStar().asSelect() - self.init(statement, database: database, animation: animation) + self.init(wrappedValue: wrappedValue, statement, database: database, animation: animation) } /// Initializes this property with a query associated with the wrapped value. @@ -531,6 +550,7 @@ extension FetchAll { /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. public init( + wrappedValue: [Element] = [], _ statement: S, database: (any DatabaseReader)? = nil, animation: Animation @@ -543,6 +563,7 @@ extension FetchAll { { let statement = statement.selectStar().asSelect() sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -562,6 +583,7 @@ extension FetchAll { @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( + wrappedValue: [Element] = [], _ statement: S, database: (any DatabaseReader)? = nil, animation: Animation @@ -575,6 +597,7 @@ extension FetchAll { { let statement = statement.selectStar().asSelect() sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementPackRequest(statement: statement), database: database, @@ -592,6 +615,7 @@ extension FetchAll { /// - animation: The animation to use for user interface changes that result from changes to /// the fetched results. public init( + wrappedValue: [Element] = [], _ statement: some StructuredQueriesCore.Statement, database: (any DatabaseReader)? = nil, animation: Animation @@ -601,6 +625,7 @@ extension FetchAll { V.QueryOutput: Sendable { sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementValueRequest(statement: statement), database: database, @@ -620,6 +645,7 @@ extension FetchAll { @_disfavoredOverload @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init( + wrappedValue: [Element] = [], _ statement: some StructuredQueriesCore.Statement<(V1, repeat each V2)>, database: (any DatabaseReader)? = nil, animation: Animation @@ -630,6 +656,7 @@ extension FetchAll { repeat (each V2).QueryOutput: Sendable { sharedReader = SharedReader( + wrappedValue: wrappedValue, .fetch( FetchAllStatementPackRequest(statement: statement), database: database, diff --git a/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md index aa1e1567..86895a82 100644 --- a/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md +++ b/Sources/StructuredQueriesGRDBCore/Documentation.docc/StructuredQueriesGRDBCore.md @@ -8,11 +8,11 @@ imported when you `import SharingGRDB` or `StructuredQueriesGRDB`. This library can be used to directly execute queries built using the [StructuredQueries][sq-gh] library and a [GRDB][grdb-gh] database. -While the `SharingGRDB` module provides tools to observe queries using the `@SharedReader` property -wrapper, you will also want to execute one-off queries directly, without Sharing's APIs, especially -when it comes to `INSERT`, `UPDATE`, and `DELETE` statements. This module extends Structured -Queries' `Statement` type with `execute`, `fetchAll`, `fetchOne`, and `fetchCount` methods that -execute the query on a given GRDB database. +While the `SharingGRDB` module provides tools to observe queries using the `@FetchAll`, `@FetchOne`, +and `@Fetch` property wrappers, you will also want to execute one-off queries directly, especially +when it comes to `INSERT`, `UPDATE`, and `DELETE` statements. This module extends +StructuredQueries' `Statement` type with `execute`, `fetchAll`, `fetchOne`, and `fetchCount` methods +that execute the query on a given GRDB database. ```swift @Table @@ -56,9 +56,9 @@ For more information on how to build queries, see the [StructuredQueries documen ### Executing statements - ``StructuredQueriesCore/Statement/execute(_:)`` -- ``StructuredQueriesCore/Statement/fetchAll(_:)-4glz5`` -- ``StructuredQueriesCore/Statement/fetchOne(_:)-3mdmq`` -- ``StructuredQueriesCore/Statement/fetchCursor(_:)-5bk5y`` +- ``StructuredQueriesCore/Statement/fetchAll(_:)`` +- ``StructuredQueriesCore/Statement/fetchOne(_:)`` +- ``StructuredQueriesCore/Statement/fetchCursor(_:)`` - ``StructuredQueriesCore/SelectStatement/fetchCount(_:)`` ### Iterating over rows diff --git a/Tests/SharingGRDBTests/IntegrationTests.swift b/Tests/SharingGRDBTests/IntegrationTests.swift index 2122674c..e80214a8 100644 --- a/Tests/SharingGRDBTests/IntegrationTests.swift +++ b/Tests/SharingGRDBTests/IntegrationTests.swift @@ -11,8 +11,7 @@ struct IntegrationTests { @Test func fetchAll_SQLString() async throws { - @SharedReader(.fetchAll(SyncUp.where(\.isActive))) - var syncUps: [SyncUp] = [] + @FetchAll(SyncUp.where(\.isActive)) var syncUps: [SyncUp] #expect(syncUps == []) try await database.write { db in @@ -37,8 +36,7 @@ struct IntegrationTests { @Test func fetch_FetchKeyRequest() async throws { - @SharedReader(.fetch(ActiveSyncUps())) - var syncUps: [SyncUp] = [] + @Fetch(ActiveSyncUps()) var syncUps: [SyncUp] = [] #expect(syncUps == []) try await database.write { db in From 2ca57d51c8699abec3c8dc3a3774509eb0c51c01 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 18:28:40 -0700 Subject: [PATCH 164/171] wip --- Tests/SharingGRDBTests/SharingGRDBTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SharingGRDBTests/SharingGRDBTests.swift b/Tests/SharingGRDBTests/SharingGRDBTests.swift index ad17ce16..bf81c3e1 100644 --- a/Tests/SharingGRDBTests/SharingGRDBTests.swift +++ b/Tests/SharingGRDBTests/SharingGRDBTests.swift @@ -13,7 +13,7 @@ import Testing try await withDependencies { $0.defaultDatabase = try DatabaseQueue() } operation: { - @SharedReader(.fetchOne(sql: "SELECT 1")) var bool = false + @FetchOne(#sql("SELECT 1", as: Bool.self)) var bool = false try await Task.sleep(nanoseconds: 100_000_000) #expect(bool) #expect($bool.loadError == nil) @@ -34,7 +34,7 @@ import Testing try await withDependencies { $0.defaultDatabase = try DatabaseQueue() } operation: { - @SharedReader(.fetchOne(sql: "SELEC 1")) var bool = false + @FetchOne(#sql("SELEC 1", as: Bool.self)) var bool = false #expect(bool == false) try await Task.sleep(nanoseconds: 100_000_000) #expect($bool.loadError is DatabaseError?) From 117c5239296a0449ba8f6a3fd76a8d0ac0369514 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 18:30:28 -0700 Subject: [PATCH 165/171] wip --- .../Documentation.docc/SharingGRDBCore.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md index 76ec02c5..5c817968 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md +++ b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md @@ -12,8 +12,8 @@ way back to the iOS 13 generation of targets. @Column { ```swift // SharingGRDB - @SharedReader(.fetchAll(Item.all)) - var items + @FetchAll + var items: [Item] @Table struct Item { @@ -105,11 +105,10 @@ in SwiftData: > Note: For more information on preparing a SQLite database, see . This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies, like -[`fetchAll`]( Date: Mon, 21 Apr 2025 18:33:59 -0700 Subject: [PATCH 166/171] wip --- .../Documentation.docc/Articles/Observing.md | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/Observing.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/Observing.md index e829c62c..9f13a208 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/Observing.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/Observing.md @@ -10,13 +10,14 @@ macro from SwiftData. ### SwiftUI -The `@SharedReader` property wrapper works in SwiftUI views similarly to how the `@Query` macro does -from SwiftData. You simply add a property to the view that is annotated with `@SharedReader` and -choose one of the various ways for [querying your database](): +The `@FetchAll`, `@FetchOne`, and `@Fetch` property wrappers work in SwiftUI views similarly to how +the `@Query` macro does from SwiftData. You simply add a property to the view that is annotated with +one of the various ways of [querying your database](): ```swift struct ItemsView: View { - @SharedReader(.fetchAll(Item.all)) var items + @FetchAll var items: [Item] + var body: some View { ForEach(items) { item in Text(item.name) @@ -30,27 +31,29 @@ queried data to update. ### @Observable models -The `@SharedReader` property also works in `@Observable` models (and `ObservableObject`s for pre-iOS -17 apps). You can add a property to an `@Observable` class, and its data will automatically update -when the database changes and cause any SwiftUI view using it to re-render: +SharedGRDB's property wrappers also works in `@Observable` models (and `ObservableObject`s for +pre-iOS 17 apps). You can add a property to an `@Observable` class, and its data will automatically +update when the database changes and cause any SwiftUI view using it to re-render: ```swift @Observable class ItemsModel { @ObservationIgnored - @SharedReader(.fetchAll(Item.all)) var items + @FetchAll var items: [Item] } struct ItemsView: View { + let model: ItemsModel + var body: some View { - ForEach(items) { item in + ForEach(model.items) { item in Text(item.name) } } } ``` -> Note: Due to how macros work in Swift, `@SharedReader` must be annotated with -> `@ObservationIgnored`, but that does not affect observation as `@SharedReader` handles its own +> Note: Due to how macros work in Swift, property wrappers must be annotated with +> `@ObservationIgnored`, but this does not affect observation as SharingGRDB handles its own > observation. ### UIKit @@ -61,7 +64,7 @@ then you can do roughly the following: ```swift class ItemsViewController: UICollectionViewController { - @SharedReader(.fetchAll(Item.all)) var items + @FetchAll var items: [Item] override func viewDidLoad() { // Set up data source and cell registration... @@ -79,8 +82,8 @@ class ItemsViewController: UICollectionViewController { } ``` -This uses the `publisher` property that is available on every `SharedReader` value to update the -collection view's data source whenever the `items` change. +This uses the `publisher` property that is available on every fetched value to update the collection +view's data source whenever the `items` change. > Tip: There is an alternative way to observe changes to `items`. If you are already depending on > our [Swift Navigation][swift-nav-gh] library to make use of powerful navigation APIs for SwiftUI From 28f114b0f13fb672302b9e2385ceccd0ecba7288 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 19:21:47 -0700 Subject: [PATCH 167/171] wip --- .../Documentation.docc/Articles/Fetching.md | 275 ++++++++++-------- 1 file changed, 161 insertions(+), 114 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md index 59cddf1a..bd4b806c 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md @@ -9,146 +9,220 @@ The primary difference between these choices is whether if you want to fetch a c rows, or fetch a single row (e.g. an aggegrate computation), or if you want to execute multiple queries in a single transaction. -* [@FetchAll](#) -* [@FetchOne](#) -* [@Fetch](#) +* [@FetchAll](#FetchAll) +* [@FetchOne](#FetchOne) +* [@Fetch](#Fetch) +### @FetchAll -### FetchAll +The [`@FetchAll`]() property wrapper allows you to fetch a collection of results from +your database using a SQL query. The query is created using our +[StructuredQueries][structured-queries-gh] library, which can build type-safe queries that safely +and performantly decode into Swift data types. - ----- - - -The primary differences between these choices is whether you want -to build queries with [StructuredQueries][structured-queries-gh], specify your query as a raw SQL -string, or if you want to assemble your value from one or more queries using a raw database -connection. - - * [Querying with StructuredQueries](#Querying-with-Structured-Queries) - * [Querying with SQL](#Querying-with-SQL) - * [Querying with custom request](#Querying-with-a-custom-request) - -[structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries - -### Querying with StructuredQueries - -[StructuredQueries][structured-queries-gh] is a library for building type-safe queries that safely -and performantly decode into Swift data types. To get access to these tools you must apply -the `@Table` macro to your data type that represents the table: +To get access to these tools you must apply the `@Table` macro to your data type that represents +your table: ```swift @Table -struct Item { +struct Reminder { let id: Int var title = "" - @Column(as: Date.ISO8601Representation.self) - var createdAt: Date + @Column(as: Date.ISO8601Representation?.self) + var dueAt: Date? + var isCompleted = false } ``` > Note: The `@Column` macro determines how to store the date in SQLite, which does not have a native > date data type. The `Date.ISO8601Representation` strategy stores dates as text formatted with the -> ISO-8601 standard. +> ISO-8601 standard. See [Defining your schema] for more info. -Then you can use the various query builder APIs on `Item` to fetch items from the database. For -example, to fetch all records from the table you can use -[`fetchAll`](): +[Defining your schema]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/definingyourschema + +With that done you can already sort all records from the `Reminder` table in their default order by +simply doing: ```swift -@SharedReader(.fetchAll(Item.all) -var items +@FetchAll(Reminder.all) +var reminders ``` -And if you want to sort the results, you can do so with an ordering clause: +If you want to execute a more complex query, such as one that sorts the results by the reminder's +title, then you can use the various query building APIs on `Reminder`: ```swift -@SharedReader(.fetchAll(Item.order(by: \.title)) -var items +@FetchAll(Reminder.order(by: \.title)) +var reminders ``` -Or, if you want to only compute an aggregate of the data in a table, such as the count of the rows, -you can do so using the -[`fetchOne`]() key: +Or if you want to only select the completed reminders, sorted by their titles in a descending +fashion: + +```swift +@FetchAll( + Reminder.where(\.isCompleted).order { $0.title.desc() } +) +var completedReminders +``` + +This is only the basics of what you can do with the query building tools of this library. To +learn more, be sure to check out the [documentation][sq-getting-started] of StructuredQueries. + +[sq-docs]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore + +You can even execute a SQL string to populate the data in your features: ```swift -@SharedReader(.fetchOne(Item.count())) -var itemsCount = 0 +@FetchAll( + #sql(""" + SELECT * FROM reminders where isCompleted ORDER BY title DESC + """, as: Reminder.self) +) +var completedReminders ``` -While StructuredQueries' builder is powerful, it is also stricter than SQLite, which will happily -coerce any data into any type, and some queries are more conveniently expressed through these -coercions. StructuredQueries should never get in your way, so rather than describe to the Swift -type system every explicit cast and coalesce, you can always embed SQL directly in a query using -the `#sql` macro: +This uses the `#sql` macro for constructing [safe SQL strings][sq-safe-sql-strings]. You are +automatically protected from SQL injection attacks, and it is even possible to use the static +description of your schema to prevent accidental typos: ```swift -@SharedReader( - .fetchAll( - Item.where { #sql("\($0.createdAt) > date('now', '-7 days')") } - ) +@FetchAll( + #sql(""" + SELECT \(Reminder.columns) + FROM \(Reminder.self) + WHERE \(Reminder.isCompleted) + ORDER BY \(Reminder.title) DESC + """, as: Reminder.self) ) -var items +var completedReminders +``` + +These interpolations are completely safe to do because they are statically known at compile time, +and it will minimize your risk for typos. Be sure to read the [documentation][sq-safe-sql-strings] +of StructuredQueries to see more of what `#sql` is capable of. + +It is also possible to join tables together and query for multiple pieces of data at once. For +example, suppose we have another table for lists of reminders, and each reminder belongs to +exactly one list: + +```swift +@Table +struct Reminder { + let id: Int + var title = "" + @Column(as: Date.ISO8601Representation?.self) + var dueAt: Date? + var isCompleted = false + var remindersListID: RemindersList.ID +} +@Table +struct RemindersList: Identifiable { + let id: Int + var title = "" +} ``` -The `#sql` macro will safely bind any input and even perform basic syntax validation. +And further suppose we have a feature that wants to load the title of every reminder, along with +the title of its associated list. Rather than loading all columns of all rows of both tables, which +is inefficient, we can select just the data we need. First we define a data type to hold just that +data, and decorate it with the `@Selection` macro: -You can even use `#sql` to write the entire query: +```swift +@Selection +struct Record { + let reminderTitle: String + let remindersListTitle: String +} +``` + +And then we construct a query that joins the `Reminder` table to the `RemindersList` table and +selects the titles from each table: ```swift -@SharedReader( - #sql( - """ - SELECT \(Item.columns) FROM \(Item.self) - WHERE \(Item.createdAt) > date('now', '-7 days') - """ - ) +@FetchAll( + Reminder + .join(RemindersList.all) { $0.remindersListID.eq($1.id) } + .select { + Record.Columns( + reminderTitle: $0.title, + remindersListTitle: $1.title + ) + } ) -var items: [Item] +var records ``` -The choice is up to you for each query or query fragment. To learn more, see the -[StructuredQueries documentation][structured-queries-docs]. +This is a very efficient query that selects only the bare essentials of data that the feature +needs to do its job. This kind of query is a lot more cumbersome to perform in SwiftData because +you must construct a dedicated `FetchDescriptor` value and set its `propertiesToFetch`. +[sq-safe-sql-strings]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/safesqlstrings [structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries [structured-queries-docs]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/ -### Querying with custom requests +### @FetchOne + +The [`@FetchOne`]() property wrapper works similarly to `@FetchAll`, but fetches +only a single record from the database and you must provide a default for when no record is found. +This tool can be handy for computing aggegrate data, such as the number of reminders in the +database: + +```swift +@FetchOne(Reminder.count()) +var remindersCount = 0 +``` + +You can perform any query you want in `@FetchOne`, including "where" clauses: + +```swift +@FetchOne(Reminder.where(\.isCompleted).count()) +var completedRemindersCount = 0 +``` + +You can use the `#sql` macro with `@FetchOne` to execute a safe SQL string: + +```swift +@FetchOne(#sql("SELECT count(*) FROM reminders WHERE isCompleted", as: Int.self)) +var completedRemindersCount = 0 +``` + +### @Fetch -It is also possible to execute multiple database queries to fetch data for your `@SharedReader`. +It is also possible to execute multiple database queries to fetch data for your features. This can be useful for performing several queries in a single database transaction: -Each instance of `@SharedReader` in a feature executes their queries in a separate -transaction. So, if we wanted to query for all in-stock items, as well as the count of all items -(in-stock plus out-of-stock) like so: +Each instance of `@FetchAll` in a feature executes their queries in a separate +transaction. So, if we wanted to query for all completed reminders, along with a total count of +reminders (completed and uncompleted), we could do so like this: ```swift -@SharedReader(.fetchOne(Item.count())) -var itemsCount = 0 +@FetchOne(Reminder.count()) +var remindersCount = 0 -@SharedReader(.fetchAll(Item.where(\.isInStock))) -var inStockItems +@FetchAll(Reminder.where(\.isCompleted))) +var completedReminders ``` …this is technically 2 queries run in 2 separate database transactions. Often this can be just fine, but if you have multiple queries that tend to change at the same -time (_e.g._, when items are created or deleted, `itemsCount` and `inStockItems` will change -at the same time), then you can bundle these two queries into a single transaction. +time (_e.g._, when reminders are created or deleted, `remindersCount` and `completedReminders` will +change at the same time), then you can bundle these two queries into a single transaction. To do this, one simply defines a conformance to our ``FetchKeyRequest`` protocol, and in that conformance one can use the builder tools to query the database: ```swift -struct Items: FetchKeyRequest { +struct Reminders: FetchKeyRequest { struct Value { - var inStockItems: [Item] = [] - var itemsCount = 0 + var completedReminders: [Reminder] = [] + var remindersCount = 0 } func fetch(_ db: Database) throws -> Value { try Value( - inStockItems: Item.where(\.isInStock).fetchAll(db), - itemsCount: Item.fetchCount(db) + completedReminders: Reminder.where(\.isCompleted).fetchAll(db), + remindersCount: Reminder.fetchCount(db) ) } } @@ -158,47 +232,20 @@ Here we have defined a ``FetchKeyRequest/Value`` type inside the conformance tha data we want to query for in a single transaction, and then we can construct it and return it from the ``FetchKeyRequest/fetch(_:)`` method. -With this conformance defined we can use -[`fetch`]() key to execute the query specified by -the `Items` type, and we can access the `inStockItems` and `itemsCount` properties to get to the -queried data: +With this conformance defined we can use the +[`@Fetch`]() property wrapper to execute the query specified by +the `Reminders` type, and we can access the `completedReminders` and `remindersCount` properties +to get to the queried data: ```swift -@SharedReader(.fetch(Items()) var items = Items.Value() -items.inStockItems // [Item(/* ... */), /* ... */] -items.itemsCount // 100 +@Fetch(Reminders()) var reminders = Reminders.Value() +reminders.completedReminders // [Reminder(/* ... */), /* ... */] +reminders.remindersCount // 100 ``` -> Note: A default must be provided to `@SharedReader` since it is querying for a custom data type +> Note: A default must be provided to `@Fetch` since it is querying for a custom data type > instead of a collection of data. Typically the conformances to ``FetchKeyRequest`` can even be made private and nested inside whatever type they are used in, such as SwiftUI view, `@Observable` model, or UIKit view controller. The only time it needs to be made public is if it's shared amongst many features. - -### Querying with raw SQL - -SharingGRDB also comes with a more basic set of tools that work directly with GRDB. The primary -reason you may want to use these tools and not the StructuredQueries tools is that they do not -require a macro to use (such as `@Table` and `#sql`), and so do not incur the cost of compiling -SwiftSyntax. - -There is a version of [`fetchAll`]() -key that takes a raw SQL string: - -```swift -@SharedReader(.fetchAll(sql: "SELECT * FROM items")) var items: [Item] -``` - -As well as a [`fetchOne`]() key: - -```swift -@SharedReader(.fetchOne(sql: "SELECT count(*) FROM items")) -var itemsCount = 0 -``` - -These APIs simply feed their data directly to GRDB's equivalent `Database` APIs, which means it is -up to you to safely bind arguments and avoid SQL injection. If you want to write SQL queries by -hand, consider using StructuredQueries' `#sql` macro, instead. - -[structured-queries-gh]: https://github.com/pointfreeco/swift-structured-queries From e4a0d9c0fd2099e10defabbaad2ce796aab51c65 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 19:26:44 -0700 Subject: [PATCH 168/171] wip --- Package.swift | 3 +-- .../xcshareddata/swiftpm/Package.resolved | 11 ++++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 1c2df049..b00ec79f 100644 --- a/Package.swift +++ b/Package.swift @@ -33,8 +33,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - //.package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.1.0"), - .package(path: "../swift-structured-queries") + .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.1.0"), ], targets: [ .target( diff --git a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved index 08ef77e8..002edd92 100644 --- a/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9e6f166164ed5ad66ac6a0d8c019ce10dd426bda22ac94e3ff771237609176ea", + "originHash" : "38edb3e6d2da325e556817b8d786c591fb5f311680b010e7bceea6379fc6cc4d", "pins" : [ { "identity" : "combine-schedulers", @@ -136,6 +136,15 @@ "version" : "1.18.3" } }, + { + "identity" : "swift-structured-queries", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-structured-queries", + "state" : { + "revision" : "7375bc75c4acaedffee9923e496b93fab18a7bd7", + "version" : "0.1.0" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", From 0c6c9ef5d35c7ae46112e5bb60b8fc9a91d4b7c0 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 21 Apr 2025 20:05:46 -0700 Subject: [PATCH 169/171] wip --- .../Articles/ComparisonWithSwiftData.md | 9 ++- .../MigrationGuides/MigratingTo0.2.md | 63 ++++++++++++++++--- .../Documentation.docc/SharingGRDBCore.md | 2 +- Sources/SharingGRDBCore/FetchKey.swift | 16 ++--- 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md index 2c436dee..1d4b53f2 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -10,7 +10,7 @@ SwiftUI views (including UIKit, `@Observable` models, _etc._). This article desc approaches compare in a variety of situations, such as setting up the data store, fetching data, associations, and more. - * [Designing your schema](#Designing-your-schema) + * [Defining your schema](#Defining-your-schema) * [Setting up external storage](#Setting-up-external-storage) * [Fetching data for a view](#Fetching-data-for-a-view) * [Fetching data for an @Observable model](#Fetching-data-for-an-Observable-model) @@ -22,7 +22,7 @@ associations, and more. * [Manual migrations](#Manual-migrations) * [Supported Apple platforms](#Supported-Apple-platforms) -### Designing your schema +### Defining your schema Both SharingGRDB and SwiftData come with tools to expose your data types' fields to the compiler so that type-safe and schema-safe queries can be written. SharingGRDB uses another library of ours @@ -73,6 +73,11 @@ Some key differences: * The `@Model` version of `Item` does not need an `id` field because SwiftData provides a `persistentIdentifier` to each model. +See the [documentation][sq-defining-schema] from StructuredQueries for more information on how +to define your schema. + +[sq-defining-schema]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/definingyourschema + ### Setting up external storage Both SharingGRDB and SwiftData require some work to be done at the entry point of the app in order diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md index d835e941..e300ffcd 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.2.md @@ -9,13 +9,62 @@ simplify the library, and make it more powerful. As such, we often need to depre in favor of newer ones. We recommend people update their code as quickly as possible to the newest APIs, and these guides contain tips to do so. -> Important: Before following any particular migration guide be sure you have followed all the -> preceding migration guides. - -* [FetchAll, FetchOne, Fetch](#) +* [@FetchAll, @FetchOne, @Fetch](#) * [fetchAll, fetchOne, fetch: soft-deprecated](#) * [Avoiding the cost of macros](#) - +## @FetchAll, @FetchOne, @Fetch + +SharingGRDB 0.2.0 comes with 3 brand new property wrappers that largely replace the need for +SwiftData and its `@Query` macro. In 0.1.0, one would perform queries as either a hard coded SQL +string: + +```swift +@SharedReader(.fetchAll("SELECT * FROM reminders WHERE isCompleted ORDER BY title")) +var completedReminders: [Reminder] +``` + +Or by defining a ``FetchKeyRequest`` conformance to perform a query using GRDB's query builder: + +```swift +struct CompletedReminders: FetchKeyRequest { + func fetch(_ db: Database) throws -> [Reminder] { + Reminder.all() + .where(Column("isCompleted")) + .order(Column("title")) + } +} + +@SharedReader(.fetch(CompletedReminders())) +var completedReminders +``` + +Each of these are cumbersome, and version 0.2.0 of SharingGRDB fixes it thanks to our newly +released [StructuredQueries][] library. You can now describe the query for your data in a type-safe +manner, and directly inline: + +```swift +@FetchAll(Reminde.where(\.isCompleted).order(by: \.title)) +var completedReminders: [Reminder] +``` + +Read for more information on how to use these new property wrappers. + +[StructuredQueries]: http://github.com/pointfreeco/swift-structured-queries + +## fetchAll, fetchOne, fetch: soft-deprecated + +The [`.fetchAll`](), +[`.fetchOne`](), +and [`.fetch`]() APIs have been soft-deprecated +in favor of the more modern tools described above and in . They will be hard +deprecated in a future release of SharingGRDB, and removed in 1.0. + +## Avoiding the cost of macros + +SharingGRDB introduces a macro in version 0.2.0 (in particular, the `@Table` macro), and +unfortunately macros currently come with an unfortunate cost in that you have to compile SwiftSyntax +from scratch, which can take time. If the cost of macros is too high for you, then you can depend +on the SharingGRDBCore module instead of the full SharingGRDB module. This will give you access to +only a subset of tools provided by SharingGRDB, but you will have access to all tools that were +available in version 0.1.0 of the library. diff --git a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md index 5c817968..289909be 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md +++ b/Sources/SharingGRDBCore/Documentation.docc/SharingGRDBCore.md @@ -105,7 +105,7 @@ in SwiftData: > Note: For more information on preparing a SQLite database, see . This `defaultDatabase` connection is used implicitly by SharingGRDB's strategies, like -[`@FetchAll`](): ```swift @FetchAll var items: [Item] diff --git a/Sources/SharingGRDBCore/FetchKey.swift b/Sources/SharingGRDBCore/FetchKey.swift index 041b2a32..5d4d1619 100644 --- a/Sources/SharingGRDBCore/FetchKey.swift +++ b/Sources/SharingGRDBCore/FetchKey.swift @@ -43,7 +43,7 @@ extension SharedReaderKey { /// - Parameters: /// - request: A request describing the data to fetch. /// - database: The database to read from. A value of `nil` will use - /// `@Dependency(\.defaultDatabase)``. + /// `@Dependency(\.defaultDatabase)`. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @available(iOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") @available(macOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") @@ -70,7 +70,7 @@ extension SharedReaderKey { /// - Parameters: /// - request: A request describing the data to fetch. /// - database: The database to read from. A value of `nil` will use - /// `@Dependency(\.defaultDatabase)``. + /// `@Dependency(\.defaultDatabase)`. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @available(iOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") @available(macOS, deprecated: 9999, message: "Use the '@Fetch' property wrapper, instead") @@ -99,7 +99,7 @@ extension SharedReaderKey { /// - sql: A raw SQL string describing the data to fetch. /// - arguments: Arguments to bind to the SQL statement. /// - database: The database to read from. A value of `nil` will use - /// `@Dependency(\.defaultDatabase)``. + /// `@Dependency(\.defaultDatabase)`. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @available(iOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") @available(macOS, deprecated: 9999, message: "Use '@FetchAll' and '#sql', instead") @@ -132,7 +132,7 @@ extension SharedReaderKey { /// - sql: A raw SQL string describing the data to fetch. /// - arguments: Arguments to bind to the SQL statement. /// - database: The database to read from. A value of `nil` will use - /// `@Dependency(\.defaultDatabase)``. + /// `@Dependency(\.defaultDatabase)`. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @available(iOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") @available(macOS, deprecated: 9999, message: "Use '@FetchOne' and '#sql', instead") @@ -158,7 +158,7 @@ extension SharedReaderKey { /// - Parameters: /// - request: A request describing the data to fetch. /// - database: The database to read from. A value of `nil` will use - /// `@Dependency(\.defaultDatabase)``. + /// `@Dependency(\.defaultDatabase)`. /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -184,7 +184,7 @@ extension SharedReaderKey { /// - Parameters: /// - request: A request describing the data to fetch. /// - database: The database to read from. A value of `nil` will use - /// `@Dependency(\.defaultDatabase)``. + /// `@Dependency(\.defaultDatabase)`. /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -211,7 +211,7 @@ extension SharedReaderKey { /// - sql: A raw SQL string describing the data to fetch. /// - arguments: Arguments to bind to the SQL statement. /// - database: The database to read from. A value of `nil` will use - /// `@Dependency(\.defaultDatabase)``. + /// `@Dependency(\.defaultDatabase)`. /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. @@ -244,7 +244,7 @@ extension SharedReaderKey { /// - sql: A raw SQL string describing the data to fetch. /// - arguments: Arguments to bind to the SQL statement. /// - database: The database to read from. A value of `nil` will use - /// `@Dependency(\.defaultDatabase)``. + /// `@Dependency(\.defaultDatabase)`. /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. /// - Returns: A key that can be passed to the `@SharedReader` property wrapper. From c8054a31140963c640f56bf2dde48373a422a415 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Apr 2025 20:17:49 -0700 Subject: [PATCH 170/171] wip --- .../Documentation.docc/Articles/Fetching.md | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md index bd4b806c..04e6a932 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/Fetching.md @@ -4,14 +4,14 @@ Learn about the various tools for fetching data from a SQLite database. ## Overview -All data fetching happens by using the `@FetchAll`, `@FetchOne` or `@Fetch` property wrappers. +All data fetching happens by using the `@FetchAll`, `@FetchOne`, or `@Fetch` property wrappers. The primary difference between these choices is whether if you want to fetch a collection of -rows, or fetch a single row (e.g. an aggegrate computation), or if you want to execute multiple +rows, or fetch a single row (_e.g._, an aggregate computation), or if you want to execute multiple queries in a single transaction. -* [@FetchAll](#FetchAll) -* [@FetchOne](#FetchOne) -* [@Fetch](#Fetch) + * [`@FetchAll`](#FetchAll) + * [`@FetchOne`](#FetchOne) + * [`@Fetch`](#Fetch) ### @FetchAll @@ -40,12 +40,11 @@ struct Reminder { [Defining your schema]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/definingyourschema -With that done you can already sort all records from the `Reminder` table in their default order by +With that done you can already fetch all records from the `Reminder` table in their default order by simply doing: ```swift -@FetchAll(Reminder.all) -var reminders +@FetchAll var reminders: [Reminder] ``` If you want to execute a more complex query, such as one that sorts the results by the reminder's @@ -75,9 +74,10 @@ You can even execute a SQL string to populate the data in your features: ```swift @FetchAll( - #sql(""" - SELECT * FROM reminders where isCompleted ORDER BY title DESC - """, as: Reminder.self) + #sql( + "SELECT * FROM reminders where isCompleted ORDER BY title DESC", + as: Reminder.self + ) ) var completedReminders ``` @@ -88,12 +88,15 @@ description of your schema to prevent accidental typos: ```swift @FetchAll( - #sql(""" - SELECT \(Reminder.columns) - FROM \(Reminder.self) - WHERE \(Reminder.isCompleted) - ORDER BY \(Reminder.title) DESC - """, as: Reminder.self) + #sql( + """ + SELECT \(Reminder.columns) + FROM \(Reminder.self) + WHERE \(Reminder.isCompleted) + ORDER BY \(Reminder.title) DESC + """, + as: Reminder.self + ) ) var completedReminders ``` @@ -165,7 +168,7 @@ you must construct a dedicated `FetchDescriptor` value and set its `propertiesTo The [`@FetchOne`]() property wrapper works similarly to `@FetchAll`, but fetches only a single record from the database and you must provide a default for when no record is found. -This tool can be handy for computing aggegrate data, such as the number of reminders in the +This tool can be handy for computing aggregate data, such as the number of reminders in the database: ```swift @@ -189,12 +192,12 @@ var completedRemindersCount = 0 ### @Fetch -It is also possible to execute multiple database queries to fetch data for your features. -This can be useful for performing several queries in a single database transaction: +It is also possible to execute multiple database queries to fetch data for your features. This can +be useful for performing several queries in a single database transaction: -Each instance of `@FetchAll` in a feature executes their queries in a separate -transaction. So, if we wanted to query for all completed reminders, along with a total count of -reminders (completed and uncompleted), we could do so like this: +Each instance of `@FetchAll` in a feature executes their queries in a separate transaction. So, if +we wanted to query for all completed reminders, along with a total count of reminders (completed and +uncompleted), we could do so like this: ```swift @FetchOne(Reminder.count()) @@ -206,8 +209,8 @@ var completedReminders …this is technically 2 queries run in 2 separate database transactions. -Often this can be just fine, but if you have multiple queries that tend to change at the same -time (_e.g._, when reminders are created or deleted, `remindersCount` and `completedReminders` will +Often this can be just fine, but if you have multiple queries that tend to change at the same time +(_e.g._, when reminders are created or deleted, `remindersCount` and `completedReminders` will change at the same time), then you can bundle these two queries into a single transaction. To do this, one simply defines a conformance to our ``FetchKeyRequest`` protocol, and in that From a62870aabb6cc8e2e0a46cc0a1644a5eea2bd832 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 22 Apr 2025 09:59:13 -0700 Subject: [PATCH 171/171] wip --- .../Documentation.docc/Extensions/Fetch.md | 2 ++ .../Documentation.docc/Extensions/FetchAll.md | 5 ++++ .../Documentation.docc/Extensions/FetchOne.md | 5 ++++ Sources/SharingGRDBCore/Fetch.swift | 27 +++++++++++++++-- Sources/SharingGRDBCore/FetchAll.swift | 29 +++++++++++++++++-- Sources/SharingGRDBCore/FetchOne.swift | 27 +++++++++++++++-- 6 files changed, 89 insertions(+), 6 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md index 76d62de0..d6a51b81 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/Fetch.md @@ -36,5 +36,7 @@ ### Sharing infrastructure +- ``sharedReader`` +- ``subscript(dynamicMember:)`` - ``FetchKey`` - ``FetchKeyID`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md index 526da121..457b6e99 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchAll.md @@ -32,3 +32,8 @@ - ``init(wrappedValue:database:scheduler:)`` - ``init(wrappedValue:_:database:scheduler:)`` - ``load(_:database:scheduler:)`` + +### Sharing infrastructure + +- ``sharedReader`` +- ``subscript(dynamicMember:)`` diff --git a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md index 66046ba9..d001456e 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Extensions/FetchOne.md @@ -33,3 +33,8 @@ - ``init(wrappedValue:database:scheduler:)`` - ``init(wrappedValue:_:database:scheduler:)`` - ``load(_:database:scheduler:)`` + +### Sharing infrastructure + +- ``sharedReader`` +- ``subscript(dynamicMember:)`` diff --git a/Sources/SharingGRDBCore/Fetch.swift b/Sources/SharingGRDBCore/Fetch.swift index e473aa33..b4335c49 100644 --- a/Sources/SharingGRDBCore/Fetch.swift +++ b/Sources/SharingGRDBCore/Fetch.swift @@ -14,9 +14,14 @@ /// ``` /// /// See for more information. +@dynamicMemberLookup @propertyWrapper public struct Fetch: Sendable { - private var sharedReader: SharedReader + /// The underlying shared reader powering the property wrapper. + /// + /// Shared readers come from the [Sharing](https://github.com/pointfreeco/swift-sharing) package, + /// a general solution to observing and persisting changes to external data sources. + public var sharedReader: SharedReader /// Data associated with the underlying query. public var wrappedValue: Value { @@ -31,6 +36,14 @@ public struct Fetch: Sendable { self } + /// Returns a ``sharedReader`` for the given key path. + /// + /// You do not invoke this subscript directly. Instead, Swift calls it for you when chaining into + /// a member of the underlying data type. + public subscript(dynamicMember keyPath: KeyPath) -> SharedReader { + sharedReader[dynamicMember: keyPath] + } + /// An error encountered during the most recent attempt to load data. public var loadError: (any Error)? { sharedReader.loadError @@ -123,8 +136,18 @@ extension Fetch { } } +extension Fetch: Equatable where Value: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.sharedReader == rhs.sharedReader + } +} + #if canImport(SwiftUI) - extension Fetch { + extension Fetch: DynamicProperty { + public func update() { + sharedReader.update() + } + /// Initializes this property with a request associated with the wrapped value. /// /// - Parameters: diff --git a/Sources/SharingGRDBCore/FetchAll.swift b/Sources/SharingGRDBCore/FetchAll.swift index 7aa4fc28..1b25d8ae 100644 --- a/Sources/SharingGRDBCore/FetchAll.swift +++ b/Sources/SharingGRDBCore/FetchAll.swift @@ -14,9 +14,14 @@ /// ``` /// /// See for more information. +@dynamicMemberLookup @propertyWrapper public struct FetchAll: Sendable { - private var sharedReader: SharedReader<[Element]> = SharedReader(value: []) + /// The underlying shared reader powering the property wrapper. + /// + /// Shared readers come from the [Sharing](https://github.com/pointfreeco/swift-sharing) package, + /// a general solution to observing and persisting changes to external data sources. + public var sharedReader: SharedReader<[Element]> = SharedReader(value: []) /// A collection of data associated with the underlying query. public var wrappedValue: [Element] { @@ -31,6 +36,16 @@ public struct FetchAll: Sendable { self } + /// Returns a ``sharedReader`` for the given key path. + /// + /// You do not invoke this subscript directly. Instead, Swift calls it for you when chaining into + /// a member of the underlying data type. + public subscript( + dynamicMember keyPath: KeyPath<[Element], Member> + ) -> SharedReader { + sharedReader[dynamicMember: keyPath] + } + /// An error encountered during the most recent attempt to load data. public var loadError: (any Error)? { sharedReader.loadError @@ -522,8 +537,18 @@ extension FetchAll { } } +extension FetchAll: Equatable where Element: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.sharedReader == rhs.sharedReader + } +} + #if canImport(SwiftUI) - extension FetchAll { + extension FetchAll: DynamicProperty { + public func update() { + sharedReader.update() + } + /// Initializes this property with a query that fetches every row from a table. /// /// - Parameters: diff --git a/Sources/SharingGRDBCore/FetchOne.swift b/Sources/SharingGRDBCore/FetchOne.swift index ffd950f3..d18286ea 100644 --- a/Sources/SharingGRDBCore/FetchOne.swift +++ b/Sources/SharingGRDBCore/FetchOne.swift @@ -14,9 +14,14 @@ /// ``` /// /// See for more information. +@dynamicMemberLookup @propertyWrapper public struct FetchOne: Sendable { - private var sharedReader: SharedReader + /// The underlying shared reader powering the property wrapper. + /// + /// Shared readers come from the [Sharing](https://github.com/pointfreeco/swift-sharing) package, + /// a general solution to observing and persisting changes to external data sources. + public var sharedReader: SharedReader /// A value associated with the underlying query. public var wrappedValue: Value { @@ -31,6 +36,14 @@ public struct FetchOne: Sendable { self } + /// Returns a ``sharedReader`` for the given key path. + /// + /// You do not invoke this subscript directly. Instead, Swift calls it for you when chaining into + /// a member of the underlying data type. + public subscript(dynamicMember keyPath: KeyPath) -> SharedReader { + sharedReader[dynamicMember: keyPath] + } + /// An error encountered during the most recent attempt to load data. public var loadError: (any Error)? { sharedReader.loadError @@ -530,8 +543,18 @@ extension FetchOne { } } +extension FetchOne: Equatable where Value: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.sharedReader == rhs.sharedReader + } +} + #if canImport(SwiftUI) - extension FetchOne { + extension FetchOne: DynamicProperty { + public func update() { + sharedReader.update() + } + /// Initializes this property with a query associated with the wrapped value. /// /// - Parameters: