From 7f3793a4733303d2a178edc63f643c831f2e1d38 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 21 Aug 2025 16:00:08 -0500 Subject: [PATCH 1/4] Improve some docs. --- .../Articles/ComparisonWithSwiftData.md | 86 +++++++++++++++++++ .../Articles/PreparingDatabase.md | 51 ++++++++--- 2 files changed, 125 insertions(+), 12 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md index 8a69f36a..ddf62be3 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -15,6 +15,7 @@ associations, and more. * [Fetching data for a view](#Fetching-data-for-a-view) * [Fetching data for an @Observable model](#Fetching-data-for-an-Observable-model) * [Dynamic queries](#Dynamic-queries) + * [Booleans and enums](#Booleans-and-enums) * [Creating, update and delete data](#Creating-update-and-delete-data) * [Associations](#Associations) * [Migrations](#Migrations) @@ -335,6 +336,91 @@ UI, and update the `@FetchAll` query when the `searchText` state changes. See for more information on how to execute dynamic queries in the library. +### Booleans and enums + +While it may be hard to believe at first, SwiftData does not fully support boolean or enum values +for fields of a model. Take for example this following model: + +```swift +@Model +class Reminder { + var isCompleted = false + var priority: Priority? + init(isCompleted: Bool = false, priority: Priority? = nil) { + self.isCompleted = isCompleted + self.priority = priority + } + + enum Priority: Int, Codable { + case low, medium, high + } +} +``` + +This model compiles just fine, but it very limited in what you can do with it. First, you cannot +sort by the `isCompleted` column when constructing a `@Query` because `Bool` is not `Comparable`: + +```swift +@Query(sort: [SortDescriptor(\.isCompleted)]) +var reminders: [Reminder] // 🛑 +``` + +There is no way to sort by boolean columns in SwiftData. + +Further, you cannot filter by enum columns, such as selecting only high-priority reminders: + +```swift +@Query(filter: #Predicate { $0.priority == Priority.high }) +var highPriorityReminders: [Reminder] +``` + +This will compile just fine yet crash at runtime. The only way to make this code work is to greatly +weaken your model by modeling both `isCompleted` _and_ `priority` as integers: + +```swift +@Model +class Reminder { + var isCompleted = 0 + var priority: Int? + init(isCompleted: Int = 0, priority: Int? = nil) { + self.isCompleted = isCompleted + self.priority = priority + } +} + +@Query( + filter: #Predicate { $0.priority == 2 }, + sort: [SortDescriptor(\.isCompleted)] +) +var highPriorityReminders: [Reminder] +``` + +This will now work, but of course these fields can now hold over 9 quintillion possible values when +only a few values are valid. + +On the other hand, booleans and enums work just fine in Sharing GRDB: + +```swift +@Table +struct Reminder { + var isCompleted = false + var priority: Priority? + enum Priority: Int, QueryBindable { + case low, medium, high + } +} + +@FetchAll( + Reminder + .where { $0.priority == Priority.high } + .order(by: \.isCompleted) +) +var reminders +``` + +This compiles and selects all high-priority reminders ordered by their `isCompleted` state. You +can even leave off thet type annotation for `reminders` because it is inferred from the query. + ### Creating, update and delete data To create, update and delete data from the database you must use the `defaultDatabase` dependency. diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md index d23deabe..ca7c8c0d 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md @@ -47,9 +47,9 @@ data: ``` This will prevent you from deleting rows that leave other rows with invalid associations. For -example, if a "teams" table had an association to a "sports" table, you would not be allowed to -delete a sports row unless there were no teams associated with it, or if you had specified a -cascading action (such as delete). +example, if a "reminders" table had an association to a "remindersLists" table, you would not be +allowed to delete a list row unless there were no reminders associated with it, or if you had +specified a cascading action (such as delete). We further recommend that you enable query tracing to log queries that are executed in your application. This can be handy for tracking down long-running queries, or when more queries execute @@ -208,11 +208,8 @@ database connection: + #if DEBUG + migrator.eraseDatabaseOnSchemaChange = true + #endif -+ migrator.registerMigration("Create sports table") { db in -+ // ... -+ } -+ migrator.registerMigration("Create teams table") { db in -+ // ... ++ migrator.registerMigration("Create tables") { db in ++ // Execute SQL to create tables + } + try migrator.migrate(database) return database @@ -221,6 +218,39 @@ database connection: As your application evolves you will register more and more migrations with the migrator. +It is up to you how you want to actually execute the SQL that creates your tables. There are APIs +in the community for building table definition statements using Swift code, but we personally feel +that it is simpler, more flexible and more powerful to use plain SQL strings: + +```swift +migrator.registerMigration("Create tables") { db in + try #sql(""" + CREATE TABLE "remindersLists"( + "id" INT NOT NULL PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL + ) STRICT + """) + .execute(db) + + try #sql(""" + CREATE TABLE "reminders"( + "id" INT NOT NULL PRIMARY KEY AUTOINCREMENT, + "isCompleted" INT NOT NULL DEFAULT 0, + "title" TEXT NOT NULL, + "remindersListID" INT NOT NULL REFERENCES "remindersLists"("id") ON DELETE CASCADE + ) STRICT + """) + .execute(db) +} +``` + +It may seem counterintuitive that we recommend using SQL strings for table definitions when so much +of the library provides type-safe and schema-safe tools for executing SQL. But table definition SQL +is fundamentally different from other SQL. Read [this article] from our StructuredQueries library +to learn more about this decision. + +[this article]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/definingyourschema#Table-definition-tools + That is all it takes to create, configure and migrate a database connection. Here is the code we have just written in one snippet: @@ -258,10 +288,7 @@ func appDatabase() throws -> any DatabaseWriter { #if DEBUG migrator.eraseDatabaseOnSchemaChange = true #endif - migrator.registerMigration("Create sports table") { db in - // ... - } - migrator.registerMigration("Create teams table") { db in + migrator.registerMigration("Create tables") { db in // ... } try migrator.migrate(database) From 53a6b05be4c1e8f27b7ec045f4355a5c14f4b948 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 21 Aug 2025 16:00:59 -0500 Subject: [PATCH 2/4] wip --- .../Articles/ComparisonWithSwiftData.md | 172 +++++++++--------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md index ddf62be3..c86fc936 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -15,9 +15,9 @@ associations, and more. * [Fetching data for a view](#Fetching-data-for-a-view) * [Fetching data for an @Observable model](#Fetching-data-for-an-Observable-model) * [Dynamic queries](#Dynamic-queries) - * [Booleans and enums](#Booleans-and-enums) * [Creating, update and delete data](#Creating-update-and-delete-data) * [Associations](#Associations) + * [Booleans and enums](#Booleans-and-enums) * [Migrations](#Migrations) * [Lightweight migrations](#Lightweight-migrations) * [Manual migrations](#Manual-migrations) @@ -336,91 +336,6 @@ UI, and update the `@FetchAll` query when the `searchText` state changes. See for more information on how to execute dynamic queries in the library. -### Booleans and enums - -While it may be hard to believe at first, SwiftData does not fully support boolean or enum values -for fields of a model. Take for example this following model: - -```swift -@Model -class Reminder { - var isCompleted = false - var priority: Priority? - init(isCompleted: Bool = false, priority: Priority? = nil) { - self.isCompleted = isCompleted - self.priority = priority - } - - enum Priority: Int, Codable { - case low, medium, high - } -} -``` - -This model compiles just fine, but it very limited in what you can do with it. First, you cannot -sort by the `isCompleted` column when constructing a `@Query` because `Bool` is not `Comparable`: - -```swift -@Query(sort: [SortDescriptor(\.isCompleted)]) -var reminders: [Reminder] // 🛑 -``` - -There is no way to sort by boolean columns in SwiftData. - -Further, you cannot filter by enum columns, such as selecting only high-priority reminders: - -```swift -@Query(filter: #Predicate { $0.priority == Priority.high }) -var highPriorityReminders: [Reminder] -``` - -This will compile just fine yet crash at runtime. The only way to make this code work is to greatly -weaken your model by modeling both `isCompleted` _and_ `priority` as integers: - -```swift -@Model -class Reminder { - var isCompleted = 0 - var priority: Int? - init(isCompleted: Int = 0, priority: Int? = nil) { - self.isCompleted = isCompleted - self.priority = priority - } -} - -@Query( - filter: #Predicate { $0.priority == 2 }, - sort: [SortDescriptor(\.isCompleted)] -) -var highPriorityReminders: [Reminder] -``` - -This will now work, but of course these fields can now hold over 9 quintillion possible values when -only a few values are valid. - -On the other hand, booleans and enums work just fine in Sharing GRDB: - -```swift -@Table -struct Reminder { - var isCompleted = false - var priority: Priority? - enum Priority: Int, QueryBindable { - case low, medium, high - } -} - -@FetchAll( - Reminder - .where { $0.priority == Priority.high } - .order(by: \.isCompleted) -) -var reminders -``` - -This compiles and selects all high-priority reminders ordered by their `isCompleted` state. You -can even leave off thet type annotation for `reminders` because it is inferred from the query. - ### Creating, update and delete data To create, update and delete data from the database you must use the `defaultDatabase` dependency. @@ -586,6 +501,91 @@ This style of handling associations does require you to be knowledgable in SQL t 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. +### Booleans and enums + +While it may be hard to believe at first, SwiftData does not fully support boolean or enum values +for fields of a model. Take for example this following model: + +```swift +@Model +class Reminder { + var isCompleted = false + var priority: Priority? + init(isCompleted: Bool = false, priority: Priority? = nil) { + self.isCompleted = isCompleted + self.priority = priority + } + + enum Priority: Int, Codable { + case low, medium, high + } +} +``` + +This model compiles just fine, but it very limited in what you can do with it. First, you cannot +sort by the `isCompleted` column when constructing a `@Query` because `Bool` is not `Comparable`: + +```swift +@Query(sort: [SortDescriptor(\.isCompleted)]) +var reminders: [Reminder] // 🛑 +``` + +There is no way to sort by boolean columns in SwiftData. + +Further, you cannot filter by enum columns, such as selecting only high-priority reminders: + +```swift +@Query(filter: #Predicate { $0.priority == Priority.high }) +var highPriorityReminders: [Reminder] +``` + +This will compile just fine yet crash at runtime. The only way to make this code work is to greatly +weaken your model by modeling both `isCompleted` _and_ `priority` as integers: + +```swift +@Model +class Reminder { + var isCompleted = 0 + var priority: Int? + init(isCompleted: Int = 0, priority: Int? = nil) { + self.isCompleted = isCompleted + self.priority = priority + } +} + +@Query( + filter: #Predicate { $0.priority == 2 }, + sort: [SortDescriptor(\.isCompleted)] +) +var highPriorityReminders: [Reminder] +``` + +This will now work, but of course these fields can now hold over 9 quintillion possible values when +only a few values are valid. + +On the other hand, booleans and enums work just fine in Sharing GRDB: + +```swift +@Table +struct Reminder { + var isCompleted = false + var priority: Priority? + enum Priority: Int, QueryBindable { + case low, medium, high + } +} + +@FetchAll( + Reminder + .where { $0.priority == Priority.high } + .order(by: \.isCompleted) +) +var reminders +``` + +This compiles and selects all high-priority reminders ordered by their `isCompleted` state. You +can even leave off thet type annotation for `reminders` because it is inferred from the query. + ### Migrations [grdb-migration-docs]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/migrations From 9d7b2d410de258dd1a21d35cf2d480f78f17073b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 21 Aug 2025 15:31:02 -0700 Subject: [PATCH 3/4] Update ComparisonWithSwiftData.md --- .../Documentation.docc/Articles/ComparisonWithSwiftData.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md index c86fc936..d9b49b8f 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -532,7 +532,7 @@ var reminders: [Reminder] // 🛑 There is no way to sort by boolean columns in SwiftData. -Further, you cannot filter by enum columns, such as selecting only high-priority reminders: +Further, you cannot filter by enum columns, such as selecting only high priority reminders: ```swift @Query(filter: #Predicate { $0.priority == Priority.high }) @@ -563,7 +563,7 @@ var highPriorityReminders: [Reminder] This will now work, but of course these fields can now hold over 9 quintillion possible values when only a few values are valid. -On the other hand, booleans and enums work just fine in Sharing GRDB: +On the other hand, booleans and enums work just fine in SharingGRDB: ```swift @Table @@ -583,7 +583,7 @@ struct Reminder { var reminders ``` -This compiles and selects all high-priority reminders ordered by their `isCompleted` state. You +This compiles and selects all high priority reminders ordered by their `isCompleted` state. You can even leave off thet type annotation for `reminders` because it is inferred from the query. ### Migrations From 3ff42e79780999f2c4883ddedc45a90b9d5f4b4e Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 22 Aug 2025 08:41:55 -0500 Subject: [PATCH 4/4] feedback --- .../Articles/ComparisonWithSwiftData.md | 2 +- .../Articles/PreparingDatabase.md | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md index d9b49b8f..9cac0d12 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -584,7 +584,7 @@ var reminders ``` This compiles and selects all high priority reminders ordered by their `isCompleted` state. You -can even leave off thet type annotation for `reminders` because it is inferred from the query. +can even leave off the type annotation for `reminders` because it is inferred from the query. ### Migrations diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md index ca7c8c0d..098db6ed 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md @@ -218,9 +218,12 @@ database connection: As your application evolves you will register more and more migrations with the migrator. -It is up to you how you want to actually execute the SQL that creates your tables. There are APIs -in the community for building table definition statements using Swift code, but we personally feel -that it is simpler, more flexible and more powerful to use plain SQL strings: +It is up to you how you want to actually execute the SQL that creates your tables. There are +[APIs in the community][grdb-table-definition] for building table definition statements using Swift +code, but we personally feel that it is simpler, more flexible and more powerful to use +[plain SQL strings][table-definition-tools]: + +[grdb-table-definition]: https://swiftpackageindex.com/groue/grdb.swift/v7.6.1/documentation/grdb/database/create(table:options:body:) ```swift migrator.registerMigration("Create tables") { db in @@ -246,10 +249,11 @@ migrator.registerMigration("Create tables") { db in It may seem counterintuitive that we recommend using SQL strings for table definitions when so much of the library provides type-safe and schema-safe tools for executing SQL. But table definition SQL -is fundamentally different from other SQL. Read [this article] from our StructuredQueries library -to learn more about this decision. +is fundamentally different from other SQL as it is frozen in time and should never be edited +after it has been deployed to users. Read [this article][table-definition-tools] from our +StructuredQueries library to learn more about this decision. -[this article]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/definingyourschema#Table-definition-tools +[table-definition-tools]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/definingyourschema#Table-definition-tools That is all it takes to create, configure and migrate a database connection. Here is the code we have just written in one snippet: