From 4bce0f03faaef46a02c9e96f081f54738be4b78c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 18 Sep 2025 21:17:49 -0500 Subject: [PATCH 01/11] Miscellaneous docs updates. --- Examples/SyncUpTests/SyncUpFormTests.swift | 1 - .../CloudKit/DefaultSyncEngine.swift | 1 - Sources/SQLiteData/CloudKit/SyncEngine.swift | 1 - .../Articles/AddingToGRDB.md | 157 ++++++++++++++++++ .../Documentation.docc/SQLiteData.md | 1 + .../Internal/FetchKey+SwiftUI.swift | 1 - .../SQLiteData/Internal/UserDatabase.swift | 1 - .../CustomFunctions.swift | 1 - .../DefaultDatabase.swift | 1 - .../StructuredQueries+GRDB/Seed.swift | 1 - .../Statement+GRDB.swift | 2 - .../SQLiteDataTestSupport/AssertQuery.swift | 1 - .../CloudKitTests/AssetsTests.swift | 67 ++++++++ .../SharingPermissionsTests.swift | 1 - .../CloudKitTests/SharingTests.swift | 1 - .../Internal/BaseCloudKitTests.swift | 2 +- .../Internal/UserDatabaseHelpers.swift | 1 - 17 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md diff --git a/Examples/SyncUpTests/SyncUpFormTests.swift b/Examples/SyncUpTests/SyncUpFormTests.swift index cc383b36..e428c38e 100644 --- a/Examples/SyncUpTests/SyncUpFormTests.swift +++ b/Examples/SyncUpTests/SyncUpFormTests.swift @@ -1,7 +1,6 @@ import Dependencies import DependenciesTestSupport import Foundation -import GRDB import StructuredQueries import Testing diff --git a/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift b/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift index 93b34516..c9582f21 100644 --- a/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift @@ -1,7 +1,6 @@ #if canImport(CloudKit) import CloudKit import Dependencies - import GRDB @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension DependencyValues { diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 4f9a5d29..84c6fa2e 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -3,7 +3,6 @@ import ConcurrencyExtras import CustomDump import Dependencies - import GRDB import OrderedCollections import OSLog import Observation diff --git a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md new file mode 100644 index 00000000..f5e24107 --- /dev/null +++ b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md @@ -0,0 +1,157 @@ +# Adding to an existing GRDB application + +Learn how to add SQLiteData to an existing app that uses GRDB. + +## Overview + +[GRDB] is a powerful SQLite library for Swift applications, and it is what is used by SQLiteData +to interact with SQLite under the hood, such as performing queries and observing changes to the +database. If you have an existing application using GRDB, and would like to use the tools of this +library, such as [`@FetchAll`](), the SQL query builder, and +[CloudKit synchronization](), then there are a few steps you must take. + +## Replace PersistableRecord and FetchableRecord with @Table + +The `PersistableRecord` and `FetchableRecord` protocols in GRDB facilitate saving data to the +database and querying for data in the database. In SQLiteData, the `@Table` macro is responsible +for this functionality. + +```diff +-struct Reminder: MutablePersistableRecord, Encodable { ++@Table ++struct Reminder { + … + } +``` + +> Tip: For an incremental migration you can use all 3 of `PersistableRecord`, `FetchableRecord` +_and_ `@Table`. That will allow you to use the query building tools from both GRDB and SQLiteData +as you transition. + +Once that is done you will be able to make use of the type-safe and schema-safe query building +tools of this library: + +```swift +RemindersList + .group(by: \.id) + .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) } + .select { + ($0.title, $1.count()) + } +} +``` + +And you can use the various property wrappers for fetching data from the database in your views +and observable models: + +```swift +@Observable +class RemindersModel { + @ObservationIgnored + @FetchAll(Reminder.order(by: \.isCompleted)) var reminders +} +``` + +> Note: Due to the fact that macros and property wrappers do not play nicely together, we are forced +> to use `@ObservationIgnored`. However, [`@FetchAll`]() handles all of its own +> observation internally and so this does not affect observation. + +There are 2 main things to be aware of when applying `@Table` to an existing schema: + +* The `@Table` macro infers the name of the SQL table from the name of the type by lowercasing the +first letter and attempting to pluralize the type. To override this default behavior, and to align +it with your current naming scheme, you can provide a string argument to `@Table`: + + ```swift + @Table("reminder") + struct Reminder { + … + } + @Table("reminders_list") + struct RemindersList { + … + } + ``` + +* If the column names of your SQLite table do not match the name of the fields in your Swift type, +then you can provide custom names via the `@Column` macro: + + ```swift + @Table + struct Reminder { + let id: UUID + var title = "" + @Column("is_completed") + var isCompleted = false + } + ``` + +## Non-optional primary keys + +Some of your data types may have an optional primary key and a `didInsert` callback for setting the +ID after insert: + +```swift +struct Reminder: MutablePersistableRecord, Encodable { + var id: Int? + var title = "" + mutating func didInsert(_ inserted: InsertionSuccess) { + id = inserted.rowID + } +} +``` + +These can be updated to use non-optional types for the primary key, and the field can be bound as +an immutable `let`: + +```swift +@Table +struct Reminder { + let id: Int + var title = "" +} +``` + +The `@Table` macro automatically generates a `Draft` type that can be used when you want to be +able to construct a value without the ID specified: + +```swift +let draft = Reminder.Draft(title: "Get milk") +``` + +Then when this draft value is inserted its ID will be determined by the database: + +```swift +try Reminder.insert { + Reminder.Draft(title: "Get milk") +} +.execute(db) +``` + +You can even use a "RETURNING" clause to grab the ID of the freshly inserted record: + +```swift +try Reminder.insert { + Reminder.Draft(title: "Get milk") +} +.returning(\.id) +.fetchOne(db) +``` + +## CloudKit synchronization + +The library's [CloudKit]() synchronization tools require that the tables being +synchronized have a primary key, and this is enforced through the `PrimaryKeyedTable` protocol. +The `@Table` macro automatically applies this protocol for you when your type has an `id` field, +but if you use a different name for your primary key you will need to use the `@Column` macro +to specify that: + +```swift +@Table struct Reminder { + @Column(primaryKey: true) + let identifier: String + … +} +``` + +[GRDB]: http://github.com/groue/GRDB.swift diff --git a/Sources/SQLiteData/Documentation.docc/SQLiteData.md b/Sources/SQLiteData/Documentation.docc/SQLiteData.md index 81647ae7..b15d5f04 100644 --- a/Sources/SQLiteData/Documentation.docc/SQLiteData.md +++ b/Sources/SQLiteData/Documentation.docc/SQLiteData.md @@ -287,6 +287,7 @@ with SQLite to take full advantage of GRDB and SQLiteData. - - - +- - ### Database configuration and access diff --git a/Sources/SQLiteData/Internal/FetchKey+SwiftUI.swift b/Sources/SQLiteData/Internal/FetchKey+SwiftUI.swift index 83abd437..078360ae 100644 --- a/Sources/SQLiteData/Internal/FetchKey+SwiftUI.swift +++ b/Sources/SQLiteData/Internal/FetchKey+SwiftUI.swift @@ -1,5 +1,4 @@ #if canImport(SwiftUI) - import GRDB import Sharing import SwiftUI diff --git a/Sources/SQLiteData/Internal/UserDatabase.swift b/Sources/SQLiteData/Internal/UserDatabase.swift index 520e8601..5f03f1a5 100644 --- a/Sources/SQLiteData/Internal/UserDatabase.swift +++ b/Sources/SQLiteData/Internal/UserDatabase.swift @@ -1,5 +1,4 @@ import Dependencies -import GRDB package struct UserDatabase { package let database: any DatabaseWriter diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/CustomFunctions.swift b/Sources/SQLiteData/StructuredQueries+GRDB/CustomFunctions.swift index 8ea3961f..f7f91590 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/CustomFunctions.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/CustomFunctions.swift @@ -1,5 +1,4 @@ import Foundation -import GRDB import GRDBSQLite extension Database { diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift b/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift index fb3a044e..63beb0fe 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift @@ -1,6 +1,5 @@ import Dependencies import Foundation -import GRDB /// Prepares a context-sensitive database writer. /// diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/Seed.swift b/Sources/SQLiteData/StructuredQueries+GRDB/Seed.swift index e161d5ea..f124b70e 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/Seed.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/Seed.swift @@ -1,5 +1,4 @@ import Dependencies -import GRDB import StructuredQueriesCore extension Database { diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift b/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift index 5f025f08..a9943ed4 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/Statement+GRDB.swift @@ -1,5 +1,3 @@ -import GRDB -import GRDBSQLite import StructuredQueriesCore extension StructuredQueriesCore.Statement { diff --git a/Sources/SQLiteDataTestSupport/AssertQuery.swift b/Sources/SQLiteDataTestSupport/AssertQuery.swift index 12cacf50..06684db1 100644 --- a/Sources/SQLiteDataTestSupport/AssertQuery.swift +++ b/Sources/SQLiteDataTestSupport/AssertQuery.swift @@ -1,7 +1,6 @@ import CustomDump import Dependencies import Foundation -import GRDB import InlineSnapshotTesting import SQLiteData import StructuredQueriesCore diff --git a/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift index 2be6702b..46d70158 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift @@ -5,6 +5,7 @@ import InlineSnapshotTesting import OrderedCollections import SQLiteData + import SQLiteDataTestSupport import SnapshotTestingCustomDump import Testing @@ -127,6 +128,8 @@ } } + // * Receive record with CKAsset from CloudKit + // => Stored in database as bytes @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func receiveAsset() async throws { let remindersListRecord = CKRecord( @@ -169,6 +172,70 @@ #expect(remindersListAsset.coverImage == Data("image".utf8)) } } + + // * Client receives RemindersListAsset with image data + // * A moment later client receives the parent RemindersList + // => Both records (and the image data) should be synchronized + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func assetReceivedBeforeParentRecord() async throws { + let remindersListRecord = CKRecord( + recordType: RemindersList.tableName, + recordID: RemindersList.recordID(for: 1) + ) + remindersListRecord.setValue("1", forKey: "id", at: now) + remindersListRecord.setValue("Personal", forKey: "title", at: now) + + let remindersListAssetRecord = CKRecord( + recordType: RemindersListAsset.tableName, + recordID: RemindersListAsset.recordID(for: 1) + ) + remindersListAssetRecord.setValue("1", forKey: "id", at: now) + remindersListAssetRecord.setValue( + Array("image".utf8), + forKey: "coverImage", + at: now + ) + remindersListAssetRecord.setValue( + "1", + forKey: "remindersListID", + at: now + ) + remindersListAssetRecord.parent = CKRecord.Reference( + record: remindersListRecord, + action: .none + ) + + let remindersListModification = try syncEngine.modifyRecords( + scope: .private, + saving: [remindersListRecord] + ) + try await syncEngine.modifyRecords(scope: .private, saving: [remindersListAssetRecord]) + .notify() + await remindersListModification.notify() + + assertQuery(RemindersList.all, database: userDatabase.database) { + """ + ┌─────────────────────┐ + │ RemindersList( │ + │ id: 1, │ + │ title: "Personal" │ + │ ) │ + └─────────────────────┘ + """ + } + assertQuery(RemindersListAsset.all, database: userDatabase.database) { + """ + ┌──────────────────────────────┐ + │ RemindersListAsset( │ + │ id: 1, │ + │ coverImage: Data(5 bytes), │ + │ remindersListID: 1 │ + │ ) │ + └──────────────────────────────┘ + """ + } + + } } } #endif diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingPermissionsTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingPermissionsTests.swift index 2f867b2f..77507724 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingPermissionsTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingPermissionsTests.swift @@ -2,7 +2,6 @@ import CloudKit import CustomDump import Foundation - import GRDB import InlineSnapshotTesting import OrderedCollections import SQLiteData diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index 860b4286..e9257179 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -3,7 +3,6 @@ import CustomDump import SQLiteDataTestSupport import Foundation - import GRDB import InlineSnapshotTesting import OrderedCollections import SQLiteData diff --git a/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift b/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift index ba37e57b..5e34de4e 100644 --- a/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift +++ b/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift @@ -7,7 +7,7 @@ import Testing import os @Suite( - .snapshots(record: .failed), + .snapshots(record: .missing), .dependencies { $0.currentTime.now = 0 $0.dataManager = InMemoryDataManager() diff --git a/Tests/SQLiteDataTests/Internal/UserDatabaseHelpers.swift b/Tests/SQLiteDataTests/Internal/UserDatabaseHelpers.swift index 710c6b07..da9b09cd 100644 --- a/Tests/SQLiteDataTests/Internal/UserDatabaseHelpers.swift +++ b/Tests/SQLiteDataTests/Internal/UserDatabaseHelpers.swift @@ -1,4 +1,3 @@ -import GRDB import SQLiteData extension UserDatabase { From 01aa3e95a88b86fcee565cc49496d56ac93af651 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 18 Sep 2025 21:31:09 -0500 Subject: [PATCH 02/11] wip --- .../Documentation.docc/Articles/CloudKit.md | 63 ++++++++++++++----- .../Articles/CloudKitSharing.md | 1 + 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md index 6e2c2350..ddcee68a 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md @@ -187,6 +187,18 @@ CREATE TABLE "reminders" ( Registering custom database functions for ID generation also makes it possible to generate deterministic IDs for tests, making it easier to test your queries. +> Important: The primary key of a row is encoded into the `recordName` of a `CKRecord`, along with +> the table name. There are [restrictions][CKRecord.ID] on the value of `recordName`: +> +> * It may only contain ASCII characters +> * It must be less than 255 characters +> * It must not begin with an underscore +> +> If your primary key violates any of these rules, a `DatabaseError` will be thrown with a message +> of ``SyncEngine/invalidRecordNameError``. + +[CKRecord.ID]: https://developer.apple.com/documentation/cloudkit/ckrecord/id + #### Primary keys on every table > TL;DR: Each synchronized table must have a single, non-compound primary key to aid in @@ -213,6 +225,25 @@ facilitate synchronizing to CloudKit. > TL;DR: Foreign key constraints can be enabled and you can use `ON DELETE` actions to > cascade deletions. +Foreign keys are a SQL feature that allow one to express relationships between tables. This library +uses that information to correctly implement synchronization behavior, such as knowing what order +to syncrhonize records (parent first, then children), and knowing what associated records to +share when sharing a root record. + +To express a foreign key relationship between tables you use the "REFERENCES" clause in the table's +schema, along with optional "ON DELETE" and "ON UPDATE" qualifiers: + +```sql +CREATE TABLE "reminders"( + "id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()), + "title" TEXT NOT NULL DEFAULT '', + "remindersListID" TEXT NOT NULL REFERENCES "remindersLists"("id") ON DELETE CASCADE +) STRICT +``` + +> Tip: See SQLite's documentation on [foreign keys] for more information. +[foreign keys]: https://sqlite.org/foreignkeys.html + SQLiteData can synchronize many-to-one and many-to-many relationships to CloudKit, and you can enforce foreign key constraints in your database connection. While it is possible for the sync engine to receive records in an order that could cause a foreign key constraint failure, @@ -241,7 +272,22 @@ when a ``SyncEngine`` is first created. If a uniqueness constraint is detected a thrown. Sometimes it is possible to make the column that you want to be unique into the primary key of -your table. For example, tags with a unique title could be modeled like so: +your table. For example, if you wanted to associate a `RemindersListAsset` type to a +`RemindersList` type, you can make the primary key of the former also act as the foreign key: + +```swift +@Table +struct RemindersListAsset { + @Column(primaryKey: true) + let remindersListID: RemindersList.ID + let image: Data +} +``` + +This will make it so that at least one asset can be associated with a reminders list. + + +For example, tags with a unique title could be modeled like so: ```swift @Table struct Tag { @@ -252,21 +298,6 @@ your table. For example, tags with a unique title could be modeled like so: // ) STRICT ``` -This will make it so that there can be at most one tag with a specific title. However, there are -important caveats to be aware of: - -> Important: The primary key of a row is encoded into the `recordName` of a `CKRecord`, along with -> the table name. There are [restrictions][CKRecord.ID] on the value of `recordName`: -> -> * It may only contain ASCII characters -> * It must be less than 255 characters -> * It must not begin with an underscore -> -> If your primary key violates any of these rules, a `DatabaseError` will be thrown with a message -> of ``SyncEngine/invalidRecordNameError``. - -[CKRecord.ID]: https://developer.apple.com/documentation/cloudkit/ckrecord/id - ## Backwards compatible migrations > TL;DR: Database migrations should be done carefully and with full backwards compatibility diff --git a/Sources/SQLiteData/Documentation.docc/Articles/CloudKitSharing.md b/Sources/SQLiteData/Documentation.docc/Articles/CloudKitSharing.md index 01e5e32e..78d367f3 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/CloudKitSharing.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/CloudKitSharing.md @@ -42,6 +42,7 @@ so like this: struct RemindersListView: View { let remindersList: RemindersList @State var sharedRecord: SharedRecord? + @Dependency(\.defaultSyncEngine) syncEngine var body: some View { Form { From b95579a2418ce097665a5b79aac8a70e2d085dfc Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 19 Sep 2025 09:34:09 -0500 Subject: [PATCH 03/11] wip --- Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md index ddcee68a..9e9f867b 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md @@ -135,8 +135,9 @@ version. #### Globally unique primary keys -> TL;DR: Primary keys should be globally unique identifiers, such as UUID. We further recommend -> specifying a `NOT NULL` constraint with a `ON CONFLICT REPLACE` action. +> TL;DR: Primary keys must be globally unique identifiers, such as UUID, and cannot be an +> autoincrementing integer. Further, a `NOT NULL` constraint should be specified with an +> `ON CONFLICT REPLACE` action. Primary keys are an important concept in SQL schema design, and SQLite makes it easy to add a primary key by using an `AUTOINCREMENT` integer. This makes it so that newly inserted rows get From e75ca0c725face1685b7f7cd623298f610262591 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 19 Sep 2025 11:26:11 -0500 Subject: [PATCH 04/11] wip --- .../Documentation.docc/Articles/CloudKit.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md index 9e9f867b..fedec2f0 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md @@ -287,18 +287,6 @@ struct RemindersListAsset { This will make it so that at least one asset can be associated with a reminders list. - -For example, tags with a unique title could be modeled like so: - -```swift -@Table struct Tag { - let title: String -} -// CREATE TABLE "tags" ( -// "title" TEXT NOT NULL PRIMARY KEY -// ) STRICT -``` - ## Backwards compatible migrations > TL;DR: Database migrations should be done carefully and with full backwards compatibility From de6efa1847ed66d9c939b1971e7ec100bbc5b11f Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 19 Sep 2025 12:39:14 -0500 Subject: [PATCH 05/11] wip --- .../Documentation.docc/Articles/AddingToGRDB.md | 12 ++++++++---- .../Documentation.docc/Articles/CloudKit.md | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md index f5e24107..adf99b87 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md @@ -18,7 +18,7 @@ for this functionality. ```diff -struct Reminder: MutablePersistableRecord, Encodable { -+@Table ++@Table("reminder") +struct Reminder { … } @@ -28,6 +28,9 @@ for this functionality. _and_ `@Table`. That will allow you to use the query building tools from both GRDB and SQLiteData as you transition. +> Note: The "reminder" argument is provided to `@Table` do to a naming convention difference between +> SQLiteData and GRDB. More details below. + Once that is done you will be able to make use of the type-safe and schema-safe query building tools of this library: @@ -59,15 +62,16 @@ class RemindersModel { There are 2 main things to be aware of when applying `@Table` to an existing schema: * The `@Table` macro infers the name of the SQL table from the name of the type by lowercasing the -first letter and attempting to pluralize the type. To override this default behavior, and to align -it with your current naming scheme, you can provide a string argument to `@Table`: +first letter and attempting to pluralize the type. This differs from GRDB's naming conventions, +which only lowercases the first letter of the type name. So, you will need to override `@Table`'s +default behavior by providing a string argument to the macro: ```swift @Table("reminder") struct Reminder { … } - @Table("reminders_list") + @Table("remindersList") struct RemindersList { … } diff --git a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md index fedec2f0..c989dd6c 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md @@ -136,7 +136,7 @@ version. #### Globally unique primary keys > TL;DR: Primary keys must be globally unique identifiers, such as UUID, and cannot be an -> autoincrementing integer. Further, a `NOT NULL` constraint should be specified with an +> autoincrementing integer. Further, a `NOT NULL` constraint must be specified with an > `ON CONFLICT REPLACE` action. Primary keys are an important concept in SQL schema design, and SQLite makes it easy to add a From 5ab5d57531b50e79f0b504cf7c5944858a374605 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 19 Sep 2025 10:54:54 -0700 Subject: [PATCH 06/11] Update AddingToGRDB.md --- .../Articles/AddingToGRDB.md | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md index adf99b87..bd5eaba8 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md @@ -24,12 +24,12 @@ for this functionality. } ``` -> Tip: For an incremental migration you can use all 3 of `PersistableRecord`, `FetchableRecord` -_and_ `@Table`. That will allow you to use the query building tools from both GRDB and SQLiteData -as you transition. +> Note: The `"reminder"` argument is provided to `@Table` due to a naming convention difference +> between SQLiteData and GRDB. More details below. -> Note: The "reminder" argument is provided to `@Table` do to a naming convention difference between -> SQLiteData and GRDB. More details below. +> Tip: For an incremental migration you can use all 3 of `PersistableRecord`, `FetchableRecord` +> _and_ `@Table`. That will allow you to use the query building tools from both GRDB and SQLiteData +> as you transition. Once that is done you will be able to make use of the type-safe and schema-safe query building tools of this library: @@ -61,34 +61,34 @@ class RemindersModel { There are 2 main things to be aware of when applying `@Table` to an existing schema: -* The `@Table` macro infers the name of the SQL table from the name of the type by lowercasing the -first letter and attempting to pluralize the type. This differs from GRDB's naming conventions, -which only lowercases the first letter of the type name. So, you will need to override `@Table`'s -default behavior by providing a string argument to the macro: - - ```swift - @Table("reminder") - struct Reminder { - … - } - @Table("remindersList") - struct RemindersList { - … - } - ``` - -* If the column names of your SQLite table do not match the name of the fields in your Swift type, -then you can provide custom names via the `@Column` macro: - - ```swift - @Table - struct Reminder { - let id: UUID - var title = "" - @Column("is_completed") - var isCompleted = false - } - ``` + * The `@Table` macro infers the name of the SQL table from the name of the type by lowercasing the + first letter and attempting to pluralize the type. This differs from GRDB's naming conventions, + which only lowercases the first letter of the type name. So, you will need to override `@Table`'s + default behavior by providing a string argument to the macro: + + ```swift + @Table("reminder") + struct Reminder { + // ... + } + @Table("remindersList") + struct RemindersList { + // ... + } + ``` + + * If the column names of your SQLite table do not match the name of the fields in your Swift type, + then you can provide custom names _via_ the `@Column` macro: + + ```swift + @Table + struct Reminder { + let id: UUID + var title = "" + @Column("is_completed") + var isCompleted = false + } + ``` ## Non-optional primary keys From 97afab88deeaa5025abeb03ddd9e828b2a3828d0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 19 Sep 2025 10:55:41 -0700 Subject: [PATCH 07/11] Fix formatting of code snippet in AddingToGRDB.md --- Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md index bd5eaba8..513e1829 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md @@ -132,7 +132,7 @@ try Reminder.insert { .execute(db) ``` -You can even use a "RETURNING" clause to grab the ID of the freshly inserted record: +You can even use a `RETURNING` clause to grab the ID of the freshly inserted record: ```swift try Reminder.insert { From a65e67ad3af049954f2c7e5dfbab4c737b013f9f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 19 Sep 2025 10:56:40 -0700 Subject: [PATCH 08/11] Fix formatting of foreign key reference syntax --- Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md index c989dd6c..0e0c7006 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md @@ -231,8 +231,8 @@ uses that information to correctly implement synchronization behavior, such as k to syncrhonize records (parent first, then children), and knowing what associated records to share when sharing a root record. -To express a foreign key relationship between tables you use the "REFERENCES" clause in the table's -schema, along with optional "ON DELETE" and "ON UPDATE" qualifiers: +To express a foreign key relationship between tables you use the `REFERENCES` clause in the table's +schema, along with optional `ON DELETE` and `ON UPDATE` qualifiers: ```sql CREATE TABLE "reminders"( From 0e38013cfb03f8a1a53c78ccaf7e94bcc1f14776 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 19 Sep 2025 12:58:13 -0500 Subject: [PATCH 09/11] wip --- .../Articles/AddingToGRDB.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md index 513e1829..382d256d 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md @@ -90,6 +90,31 @@ There are 2 main things to be aware of when applying `@Table` to an existing sch } ``` +* If your tables use UUID then you will need to add an extra decoration to your Swift data type +to make it compatible with SQLiteData. This is due to the fact that by default GRDB encodes UUIDs +as bytes whereas SQLiteData encodes UUIDs as text. To keep this compatibility you will need to use +`@Column(as:)` on any fields holding UUIDs: + + ```swift + @Table + struct Reminder { + @Column(as: UUID.BytesRepresentation.self) + let id: UUID + … + } + ``` + + And if your table has an optional UUID, then you will handle that similarly: + + ```swift + @Table + struct ChildReminder { + @Column(as: UUID?.BytesRepresentation.self) + let parentID: UUID? + … + } + ``` + ## Non-optional primary keys Some of your data types may have an optional primary key and a `didInsert` callback for setting the From a95323c08057f826e07eb7a2d484cd62dbb0c66c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 19 Sep 2025 12:58:17 -0500 Subject: [PATCH 10/11] wip --- Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md index 382d256d..dcf3e48d 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md @@ -59,7 +59,7 @@ class RemindersModel { > to use `@ObservationIgnored`. However, [`@FetchAll`]() handles all of its own > observation internally and so this does not affect observation. -There are 2 main things to be aware of when applying `@Table` to an existing schema: +There are 3 main things to be aware of when applying `@Table` to an existing schema: * The `@Table` macro infers the name of the SQL table from the name of the type by lowercasing the first letter and attempting to pluralize the type. This differs from GRDB's naming conventions, From 63b95d3674bdcb0e88465dbed8b7fbd2e0e0d5fa Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 19 Sep 2025 10:59:45 -0700 Subject: [PATCH 11/11] Refactor UUID handling section in AddingToGRDB.md Updated formatting and indentation for UUID handling in Swift data types to improve readability. --- .../Articles/AddingToGRDB.md | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md index dcf3e48d..2a2748c8 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/AddingToGRDB.md @@ -90,30 +90,30 @@ There are 3 main things to be aware of when applying `@Table` to an existing sch } ``` -* If your tables use UUID then you will need to add an extra decoration to your Swift data type -to make it compatible with SQLiteData. This is due to the fact that by default GRDB encodes UUIDs -as bytes whereas SQLiteData encodes UUIDs as text. To keep this compatibility you will need to use -`@Column(as:)` on any fields holding UUIDs: - - ```swift - @Table - struct Reminder { - @Column(as: UUID.BytesRepresentation.self) - let id: UUID - … - } - ``` + * If your tables use UUID then you will need to add an extra decoration to your Swift data type + to make it compatible with SQLiteData. This is due to the fact that by default GRDB encodes UUIDs + as bytes whereas SQLiteData encodes UUIDs as text. To keep this compatibility you will need to use + `@Column(as:)` on any fields holding UUIDs: - And if your table has an optional UUID, then you will handle that similarly: + ```swift + @Table + struct Reminder { + @Column(as: UUID.BytesRepresentation.self) + let id: UUID + // ... + } + ``` - ```swift - @Table - struct ChildReminder { - @Column(as: UUID?.BytesRepresentation.self) - let parentID: UUID? - … - } - ``` + And if your table has an optional UUID, then you will handle that similarly: + + ```swift + @Table + struct ChildReminder { + @Column(as: UUID?.BytesRepresentation.self) + let parentID: UUID? + // ... + } + ``` ## Non-optional primary keys