Skip to content

Commit a38c595

Browse files
Improve some docs. (#135)
* Improve some docs. * wip * Update ComparisonWithSwiftData.md * feedback --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent 24f811d commit a38c595

File tree

2 files changed

+129
-12
lines changed

2 files changed

+129
-12
lines changed

Sources/SharingGRDBCore/Documentation.docc/Articles/ComparisonWithSwiftData.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ associations, and more.
1717
* [Dynamic queries](#Dynamic-queries)
1818
* [Creating, update and delete data](#Creating-update-and-delete-data)
1919
* [Associations](#Associations)
20+
* [Booleans and enums](#Booleans-and-enums)
2021
* [Migrations](#Migrations)
2122
* [Lightweight migrations](#Lightweight-migrations)
2223
* [Manual migrations](#Manual-migrations)
@@ -500,6 +501,91 @@ This style of handling associations does require you to be knowledgable in SQL t
500501
correctly, but that is a benefit! SQL (and SQLite) are some of the most proven pieces of
501502
technologies in the history of computers, and knowing how to wield their powers is a huge benefit.
502503

504+
### Booleans and enums
505+
506+
While it may be hard to believe at first, SwiftData does not fully support boolean or enum values
507+
for fields of a model. Take for example this following model:
508+
509+
```swift
510+
@Model
511+
class Reminder {
512+
var isCompleted = false
513+
var priority: Priority?
514+
init(isCompleted: Bool = false, priority: Priority? = nil) {
515+
self.isCompleted = isCompleted
516+
self.priority = priority
517+
}
518+
519+
enum Priority: Int, Codable {
520+
case low, medium, high
521+
}
522+
}
523+
```
524+
525+
This model compiles just fine, but it very limited in what you can do with it. First, you cannot
526+
sort by the `isCompleted` column when constructing a `@Query` because `Bool` is not `Comparable`:
527+
528+
```swift
529+
@Query(sort: [SortDescriptor(\.isCompleted)])
530+
var reminders: [Reminder] // 🛑
531+
```
532+
533+
There is no way to sort by boolean columns in SwiftData.
534+
535+
Further, you cannot filter by enum columns, such as selecting only high priority reminders:
536+
537+
```swift
538+
@Query(filter: #Predicate { $0.priority == Priority.high })
539+
var highPriorityReminders: [Reminder]
540+
```
541+
542+
This will compile just fine yet crash at runtime. The only way to make this code work is to greatly
543+
weaken your model by modeling both `isCompleted` _and_ `priority` as integers:
544+
545+
```swift
546+
@Model
547+
class Reminder {
548+
var isCompleted = 0
549+
var priority: Int?
550+
init(isCompleted: Int = 0, priority: Int? = nil) {
551+
self.isCompleted = isCompleted
552+
self.priority = priority
553+
}
554+
}
555+
556+
@Query(
557+
filter: #Predicate { $0.priority == 2 },
558+
sort: [SortDescriptor(\.isCompleted)]
559+
)
560+
var highPriorityReminders: [Reminder]
561+
```
562+
563+
This will now work, but of course these fields can now hold over 9 quintillion possible values when
564+
only a few values are valid.
565+
566+
On the other hand, booleans and enums work just fine in SharingGRDB:
567+
568+
```swift
569+
@Table
570+
struct Reminder {
571+
var isCompleted = false
572+
var priority: Priority?
573+
enum Priority: Int, QueryBindable {
574+
case low, medium, high
575+
}
576+
}
577+
578+
@FetchAll(
579+
Reminder
580+
.where { $0.priority == Priority.high }
581+
.order(by: \.isCompleted)
582+
)
583+
var reminders
584+
```
585+
586+
This compiles and selects all high priority reminders ordered by their `isCompleted` state. You
587+
can even leave off the type annotation for `reminders` because it is inferred from the query.
588+
503589
### Migrations
504590

505591
[grdb-migration-docs]: https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/migrations

Sources/SharingGRDBCore/Documentation.docc/Articles/PreparingDatabase.md

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ data:
4747
```
4848

4949
This will prevent you from deleting rows that leave other rows with invalid associations. For
50-
example, if a "teams" table had an association to a "sports" table, you would not be allowed to
51-
delete a sports row unless there were no teams associated with it, or if you had specified a
52-
cascading action (such as delete).
50+
example, if a "reminders" table had an association to a "remindersLists" table, you would not be
51+
allowed to delete a list row unless there were no reminders associated with it, or if you had
52+
specified a cascading action (such as delete).
5353

5454
We further recommend that you enable query tracing to log queries that are executed in your
5555
application. This can be handy for tracking down long-running queries, or when more queries execute
@@ -208,11 +208,8 @@ database connection:
208208
+ #if DEBUG
209209
+ migrator.eraseDatabaseOnSchemaChange = true
210210
+ #endif
211-
+ migrator.registerMigration("Create sports table") { db in
212-
+ // ...
213-
+ }
214-
+ migrator.registerMigration("Create teams table") { db in
215-
+ // ...
211+
+ migrator.registerMigration("Create tables") { db in
212+
+ // Execute SQL to create tables
216213
+ }
217214
+ try migrator.migrate(database)
218215
return database
@@ -221,6 +218,43 @@ database connection:
221218

222219
As your application evolves you will register more and more migrations with the migrator.
223220

221+
It is up to you how you want to actually execute the SQL that creates your tables. There are
222+
[APIs in the community][grdb-table-definition] for building table definition statements using Swift
223+
code, but we personally feel that it is simpler, more flexible and more powerful to use
224+
[plain SQL strings][table-definition-tools]:
225+
226+
[grdb-table-definition]: https://swiftpackageindex.com/groue/grdb.swift/v7.6.1/documentation/grdb/database/create(table:options:body:)
227+
228+
```swift
229+
migrator.registerMigration("Create tables") { db in
230+
try #sql("""
231+
CREATE TABLE "remindersLists"(
232+
"id" INT NOT NULL PRIMARY KEY AUTOINCREMENT,
233+
"title" TEXT NOT NULL
234+
) STRICT
235+
""")
236+
.execute(db)
237+
238+
try #sql("""
239+
CREATE TABLE "reminders"(
240+
"id" INT NOT NULL PRIMARY KEY AUTOINCREMENT,
241+
"isCompleted" INT NOT NULL DEFAULT 0,
242+
"title" TEXT NOT NULL,
243+
"remindersListID" INT NOT NULL REFERENCES "remindersLists"("id") ON DELETE CASCADE
244+
) STRICT
245+
""")
246+
.execute(db)
247+
}
248+
```
249+
250+
It may seem counterintuitive that we recommend using SQL strings for table definitions when so much
251+
of the library provides type-safe and schema-safe tools for executing SQL. But table definition SQL
252+
is fundamentally different from other SQL as it is frozen in time and should never be edited
253+
after it has been deployed to users. Read [this article][table-definition-tools] from our
254+
StructuredQueries library to learn more about this decision.
255+
256+
[table-definition-tools]: https://swiftpackageindex.com/pointfreeco/swift-structured-queries/main/documentation/structuredqueriescore/definingyourschema#Table-definition-tools
257+
224258
That is all it takes to create, configure and migrate a database connection. Here is the code
225259
we have just written in one snippet:
226260

@@ -258,10 +292,7 @@ func appDatabase() throws -> any DatabaseWriter {
258292
#if DEBUG
259293
migrator.eraseDatabaseOnSchemaChange = true
260294
#endif
261-
migrator.registerMigration("Create sports table") { db in
262-
// ...
263-
}
264-
migrator.registerMigration("Create teams table") { db in
295+
migrator.registerMigration("Create tables") { db in
265296
// ...
266297
}
267298
try migrator.migrate(database)

0 commit comments

Comments
 (0)