diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md index 8a69f36a..9cac0d12 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md @@ -17,6 +17,7 @@ associations, and more. * [Dynamic queries](#Dynamic-queries) * [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) @@ -500,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 SharingGRDB: + +```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 the 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 diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md index d23deabe..098db6ed 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,43 @@ 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][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 + 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 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. + +[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: @@ -258,10 +292,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)