Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be important to find a way to link the information at the top of this section about grdb-migration-docs—which also explains how to perform migrations using Swift code—with this StructuredQueries way

It would also be good to highlight that there are several ways to complete the code snippet marked with // Execute SQL to create tables, and it’d be helpful to include links [or clarify] both to the GRDB approach and to the StructuredQueries approach using the #sql macro!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! Just pushed some updates.

That is all it takes to create, configure and migrate a database connection. Here is the code
we have just written in one snippet:

Expand Down Expand Up @@ -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)
Expand Down
Loading