Skip to content

Commit d5fd836

Browse files
committed
Merge remote-tracking branch 'origin/main' into tagged-trait
2 parents fe94201 + c851cb4 commit d5fd836

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+792
-413
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Run tests
2828
run: swift test
2929
- name: Build release
30-
run: swift build -c release
30+
run: swift build -c release --build-tests
3131

3232
linux:
3333
name: Linux

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ let package = Package(
4343
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.1"),
4444
.package(url: "https://github.com/pointfreeco/swift-tagged", from: "0.10.0"),
4545
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.2"),
46-
.package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"601.0.0"),
46+
.package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"602.0.0"),
4747
],
4848
targets: [
4949
.target(

Sources/StructuredQueries/Macros.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ import StructuredQueriesCore
1616
named(init(_:)),
1717
named(init(decoder:)),
1818
named(QueryValue),
19+
named(schemaName),
1920
named(tableName)
2021
)
2122
@attached(
2223
memberAttribute
2324
)
24-
public macro Table(_ name: String? = nil) =
25+
public macro Table(
26+
_ name: String? = nil,
27+
schema schemaName: String? = nil
28+
) =
2529
#externalMacro(
2630
module: "StructuredQueriesMacros",
2731
type: "TableMacro"

Sources/StructuredQueriesCore/Documentation.docc/Articles/DefiningYourSchema.md

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,19 @@ generated when writing queries will correctly use `"is_completed"`.
143143
144144
### Custom data types
145145
146-
There are many data types that do not have native support in SQLite. For these data types you must
147-
define a conformance to ``QueryBindable`` in order to translate values to a format that SQLite does
148-
understand. The library comes with conformances to aid in representing dates, UUIDs, and JSON, and
149-
you can define your own conformances for your own custom data types.
146+
StructuredQueries provides support for many basic Swift data types out of the box, like strings,
147+
integers, doubles, bytes, and booleans, but you may want to represent custom, domain specific types
148+
with your table's columns, instead. For these data types you must either define a conformance to
149+
``QueryBindable`` to translate values to a format that the library does understand, or provide a
150+
``QueryRepresentable`` type that wraps your domain type.
151+
152+
The library comes with several `QueryRepresentable` conformances to aid in representing dates,
153+
UUIDs, and JSON, and you can define your own conformances for your own custom data types.
150154
151155
#### Dates
152156
153-
SQLite does not have a native date type, and instead has 3 different ways to represent dates:
157+
While some relational databases, like MySQL and Postgres, have native support for dates, SQLite
158+
does _not_. Instead, it has 3 different ways to represent dates:
154159
155160
* Text column interpreted as ISO-8601-formatted string.
156161
* Int column interpreted as number of seconds since Unix epoch.
@@ -203,6 +208,14 @@ And StructuredQueries will take care of formatting the value for the database:
203208
}
204209
}
205210
211+
When querying against a date column with a Swift date, you will need to explicitly bundle up the
212+
Swift date into the appropriate representation to use various query helpers. This can be done using
213+
the `#bind` macro:
214+
215+
```swift
216+
Reminder.where { $0.created > #bind(startDate) }
217+
```
218+
206219
#### UUID
207220
208221
SQLite also does not have native support for UUIDs. If you try to use a UUID in your tables you
@@ -238,6 +251,14 @@ translate the UUID to text:
238251
}
239252
```
240253
254+
When querying against a UUID column with a Swift UUID, you will need to explicitly bundle up the
255+
Swift UUID into the appropriate representation to use various query helpers. This can be done using
256+
the `#bind` macro:
257+
258+
```swift
259+
Reminder.where { $0.id != #bind(reminder.id) }
260+
```
261+
241262
#### RawRepresentable
242263
243264
Simple data types, in particular ones conforming to `RawRepresentable` whose `RawValue` is a string
@@ -295,14 +316,15 @@ example, suppose the `Reminder` table had an array of notes:
295316
296317
This does not work because the `@Table` macro does not know how to encode and decode an array
297318
of strings into a value that SQLite understands. If you annotate this field with
298-
``JSONRepresentation``, then the library can encode the array of strings to a JSON string when
299-
storing data in the table, and decode the JSON array into a Swift array when decoding a row:
319+
``Swift/Decodable/JSONRepresentation``, then the library can encode the array of strings to a JSON
320+
string when storing data in the table, and decode the JSON array into a Swift array when decoding a
321+
row:
300322
301323
```swift
302324
@Table struct Reminder {
303325
let id: Int
304326
var title = ""
305-
@Column(as: JSONRepresentation<[String]>.self)
327+
@Column(as: [String].JSONRepresentation.self)
306328
var notes: [String]
307329
}
308330
```

Sources/StructuredQueriesCore/Documentation.docc/Articles/PrimaryKeyedTables.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ draft has an ID or inserting a new row if the ID is `nil`:
217217
@Row {
218218
@Column {
219219
```swift
220-
let reminder = Reminder(
220+
let reminder = Reminder.Draft(
221221
id: 1,
222222
title: "Get groceries",
223223
isCompleted: false
@@ -231,7 +231,7 @@ draft has an ID or inserting a new row if the ID is `nil`:
231231
("id", "isCompleted", "title")
232232
VALUES
233233
(1, 0, 'Get groceries')
234-
ON CONFLICT DO UPDATE SET
234+
ON CONFLICT ("id") DO UPDATE SET
235235
"isCompleted" =
236236
"excluded"."isCompleted",
237237
"title" = "excluded"."title"

Sources/StructuredQueriesCore/Documentation.docc/Articles/QueryCookbook.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ large and complex queries.
1212
* [Reusable column queries](#Reusable-column-queries)
1313
* [Default scopes](#Default-scopes)
1414
* [Custom selections](#Custom-selections)
15-
* [Pre-loading associations with JSON](#Pre-loading-associations-with-json)
15+
* [Pre-loading associations with JSON](#Pre-loading-associations-with-JSON)
1616

1717
### Reusable table queries
1818

@@ -396,19 +396,20 @@ suspiciously like a join constraint, which should give us a hint that what we ar
396396
quite right.
397397

398398
Another way to do this is to use the `@Selection` macro described above
399-
(<doc:QueryCookbook#Custom-selections>), along with a ``JSONRepresentation`` of the collection
400-
of reminders you want to load for each list:
399+
(<doc:QueryCookbook#Custom-selections>), along with a ``Swift/Decodable/JSONRepresentation`` of the
400+
collection of reminders you want to load for each list:
401401

402402
```struct
403403
@Selection
404404
struct Row {
405405
let remindersList: RemindersList
406-
@Column(as: JSONRepresentation<[Reminder]>.self)
406+
@Column(as: [Reminder].JSONRepresentation.self)
407407
let reminders: [Reminder]
408408
}
409409
```
410410

411-
> Note: `Reminder` must conform to `Codable` to be able to use ``JSONRepresentation``.
411+
> Note: `Reminder` must conform to `Codable` to be able to use
412+
> ``Swift/Decodable/JSONRepresentation``.
412413
413414
This allows the query to serialize the associated rows into JSON, which are then deserialized into
414415
a `Row` type. To construct such a query you can use the

Sources/StructuredQueriesCore/Documentation.docc/Articles/SafeSQLStrings.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,27 @@ could be written as a single invocation of the macro:
108108
All of the columns provided to trailing closures in the query builder are available statically on
109109
each table type, so you can freely interpolate this schema information into the SQL string.
110110

111+
> Important: _Always_ interpolate as much static schema information as possible into the SQL string
112+
> to better ensure that queries are correct and will successfully decode.
113+
>
114+
> For example:
115+
>
116+
> ```diff
117+
> -SELECT * FROM reminders
118+
> +SELECT \(Reminder.columns) FROM \(Reminder.self)
119+
> ```
120+
>
121+
> * Selecting "`*`" requires that the column order in the database matches the field order in the
122+
> Swift data type. Because StructuredQueries decodes columns in positional order, a query using
123+
> "`*`" will fail to decode unless the field order matches exactly. Instead of leaving this to
124+
> chance, prefer interpolating `Table.columns`, which will generate an explicit SQL column
125+
> selection that matches the order of fields in the Swift data type.
126+
> * Spelling out table and column names directly inside the query (_e.g._ "`reminders`") can lead
127+
> to runtime errors due to typos or stale queries that refer to schema columns that have been
128+
> renamed or removed. Instead, prefer interpolating `Table.columnName` to refer to a particular
129+
> column (_e.g._, `Reminder.isCompleted`), and `Table.self` to refer to a table (_e.g._,
130+
> `Reminder.self`).
131+
111132
Note that the query's represented type cannot be inferred here, and so the `as` parameter is used
112133
to let Swift know that we expect to decode the `Reminder` type when we execute the query.
113134
@@ -131,11 +152,12 @@ Values can be interpolated into `#sql` strings to produce dynamic queries:
131152
@Column {
132153
```swift
133154
let isCompleted = true
155+
134156
#sql(
135157
"""
136158
SELECT count(*)
137-
FROM reminders
138-
WHERE isCompleted = \(isCompleted)
159+
FROM \(Reminder.self)
160+
WHERE \(Reminder.isCompleted) = \(isCompleted)
139161
""",
140162
as: Reminder.self
141163
)
@@ -144,22 +166,22 @@ Values can be interpolated into `#sql` strings to produce dynamic queries:
144166
@Column {
145167
```sql
146168
SELECT count(*)
147-
FROM reminders
148-
WHERE isCompleted = ?
169+
FROM "reminders"
170+
WHERE "reminders"."isCompleted" = ?
149171
-- [1]
150172
```
151173
}
152174
}
153175

154-
Note that although it seems the literal value is being interpolated directly into the string, that
176+
Note that although it seems that `isCompleted` is being interpolated directly into the string, that
155177
is not what is happening. The interpolated value is captured as a separate statement binding in
156178
order to protect against SQL injection.
157179

158-
String bindings are handled in a special fashion to make it clear what the intended usage is. If
159-
you interpolate a string into a `#sql` string, you will get a deprecation warning:
180+
String bindings are handled in a special fashion to make it clear what the intended usage is. If you
181+
interpolate a string into a `#sql` string, you will get a deprecation warning:
160182

161183
```swift
162-
let searchText = "get"
184+
let searchText = "%get%"
163185
#sql(
164186
"""
165187
SELECT \(Reminder.columns)
@@ -178,7 +200,7 @@ If you mean to bind the string as a value, you can update the interpolation to u
178200
@Row {
179201
@Column {
180202
```swift
181-
let searchText = "get"
203+
let searchText = "%get%"
182204
#sql(
183205
"""
184206
SELECT \(Reminder.columns)
@@ -205,12 +227,12 @@ If you mean to interpolate the string directly into the SQL you can use
205227
@Row {
206228
@Column {
207229
```swift
208-
let searchText = "get"
230+
let searchText = "%get%"
209231
#sql(
210232
"""
211233
SELECT \(Reminder.columns)
212234
FROM \(Reminder.self)
213-
WHERE \(Reminder.title) COLLATE NOCASE LIKE '%\(raw: searchText)%'
235+
WHERE \(Reminder.title) COLLATE NOCASE LIKE '\(raw: searchText)'
214236
""",
215237
as: Reminder.self
216238
)

Sources/StructuredQueriesCore/Documentation.docc/Articles/SelectStatements.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,8 @@ The `limit(_:offset:)` function is used to change a query's `LIMIT` and `OFFSET`
721721
}
722722
}
723723

724-
Multiple chained calls to `limit` will override the limit and offset to the last call:
724+
Multiple chained calls to `limit` will override the limit and offset to the last call, using the
725+
existing offset if none is provided:
725726

726727
@Row {
727728
@Column {
@@ -755,6 +756,22 @@ Multiple chained calls to `limit` will override the limit and offset to the last
755756
}
756757
}
757758

759+
@Row {
760+
@Column {
761+
```swift
762+
Reminder
763+
.limit(10, offset: 10)
764+
.limit(20, offset: 20)
765+
```
766+
}
767+
@Column {
768+
```sql
769+
SELECT … FROM "reminders"
770+
LIMIT 20 OFFSET 20
771+
```
772+
}
773+
}
774+
758775
### Compound selects
759776

760777
It is possible to combine multiple selects together using the `union`, `intersect`, and `except`

Sources/StructuredQueriesCore/Documentation.docc/Extensions/QueryRepresentable.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
- ``Foundation/UUID/BytesRepresentation``
1717
- ``Foundation/UUID/LowercasedRepresentation``
1818
- ``Foundation/UUID/UppercasedRepresentation``
19-
- ``JSONRepresentation``
19+
- ``Swift/Decodable/JSONRepresentation``

0 commit comments

Comments
 (0)