Skip to content

Commit fd1beb8

Browse files
Update docs on temporary views. (#182)
* Update docs on temporary views. * wip * wip * wip * wip * Allow table selections to have primary key. * wip * wip * wip * Fix formatting and syntax in Views.md documentation * Update Views.md * wip * wip --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent e45b611 commit fd1beb8

File tree

6 files changed

+483
-6
lines changed

6 files changed

+483
-6
lines changed

Sources/StructuredQueries/Documentation.docc/StructuredQueries.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ A library for building SQL in a type-safe, expressive, and composable manner.
55
## Overview
66

77
The core functionality of this library is defined in
8-
[`StructuredQueriesCore`](<doc:/StructuredQueriesCore>), which this module automatically exports.
8+
[`StructuredQueriesCore`](structuredqueriescore), which this module automatically exports.
99

1010
This module also contains all of the macros that support the core functionality of the library.
1111

12-
See [`StructuredQueriesCore`](<doc:/StructuredQueriesCore>) for general library usage.
12+
See [`StructuredQueriesCore`](structuredqueriescore) for general library usage.
1313

1414
StructuredQueries also ships SQLite-specific helpers:
1515

16-
- [`StructuredQueriesSQLiteCore`](<doc:/StructuredQueriesSQLiteCore>): Core, SQLite-specific
16+
- [`StructuredQueriesSQLiteCore`](structuredqueriessqlitecore): Core, SQLite-specific
1717
functionality, including full-text search, type-safe temporary triggers, full-text search, and
1818
more.
1919

20-
- [`StructuredQueriesSQLite`](<doc:/StructuredQueriesSQLite>): Everything from
20+
- [`StructuredQueriesSQLite`](structuredqueriessqlitecore): Everything from
2121
`StructuredQueriesSQLiteCore` and macros that support it, like `@DatabaseFunction.`
2222

2323
## Topics

Sources/StructuredQueriesMacros/SelectionMacro.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ extension SelectionMacro: ExtensionMacro {
9898
columnQueryValueType = "\(raw: base.trimmedDescription)"
9999

100100
case .some(let label) where label.text == "primaryKey":
101+
guard !declaration.hasMacroApplication("Table")
102+
else { continue }
103+
101104
var newArguments = arguments
102105
newArguments.remove(at: argumentIndex)
103106
diagnostics.append(
@@ -257,6 +260,9 @@ extension SelectionMacro: MemberMacro {
257260
columnQueryValueType = "\(raw: base.trimmedDescription)"
258261

259262
case .some(let label) where label.text == "primaryKey":
263+
guard !declaration.hasMacroApplication("Table")
264+
else { continue }
265+
260266
var newArguments = arguments
261267
newArguments.remove(at: argumentIndex)
262268
expansionFailed = true
@@ -329,7 +335,7 @@ extension SelectionMacro: MemberMacro {
329335
"""
330336
\($0): some \(moduleName).QueryExpression\
331337
\($1.map { "<\($0)>" } ?? "")\
332-
\($2.map { "= #bind(\($0))" } ?? "")
338+
\($2.map { "= \(moduleName).BindQueryExpression(\($0))" } ?? "")
333339
"""
334340
}
335341
.joined(separator: ",\n")

Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Views.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,139 @@ Learn how to create views that can be queried.
88
be queried like a table. StructuredQueries comes with tools to create _temporary_ views in a
99
type-safe and schema-safe fashion.
1010

11+
### Creating temporary views
12+
13+
To define a view into your database you must first define a Swift data type that holds the data
14+
you want to query for. As a simple example, suppose we want a view into the database that selects
15+
the title of each reminder, along with the title of each list, we can model this as a simple
16+
Swift struct:
17+
18+
```swift
19+
@Table @Selection
20+
private struct ReminderWithList {
21+
let reminderTitle: String
22+
let remindersListTitle: String
23+
}
24+
```
25+
26+
Note that we have applied both the `@Table` macro and `@Selection` macro. This is similar to what
27+
one does with common table expressions, and it allows one to represent a type that for intents and
28+
purposes seems like a regular SQLite table, but it's not actually persisted in the database.
29+
30+
With that type defined we can use the
31+
``StructuredQueriesCore/Table/createTemporaryView(ifNotExists:as:)`` to create a SQL query that
32+
creates a temporary view. You provide a select statement that selects all the data needed for the
33+
view:
34+
35+
```swift
36+
ReminderWithList.createTemporaryView(
37+
as: Reminder
38+
.join(RemindersList.all) { $0.remindersListID.eq($1.id) }
39+
.select {
40+
ReminderWithList.Columns(
41+
reminderTitle: $0.title,
42+
remindersListTitle: $1.title
43+
)
44+
}
45+
)
46+
```
47+
48+
Once that is executed in your database you are free to query from this table as if it is a regular
49+
table:
50+
51+
@Row {
52+
@Column {
53+
```swift
54+
ReminderWithList
55+
.order {
56+
($0.remindersListTitle,
57+
$0.reminderTitle)
58+
}
59+
.limit(3)
60+
```
61+
}
62+
@Column {
63+
```sql
64+
SELECT
65+
"reminderWithLists"."reminderTitle",
66+
"reminderWithLists"."remindersListTitle"
67+
FROM "reminderWithLists"
68+
ORDER BY
69+
"reminderWithLists"."remindersListTitle",
70+
"reminderWithLists"."reminderTitle"
71+
LIMIT 3
72+
```
73+
}
74+
}
75+
76+
The best part of this is that the `JOIN` used in the view is completely hidden from us. For all
77+
intents and purposes, `ReminderWithList` seems like a regular SQL table for which each row holds
78+
just two strings. We can simply query from the table to get that data in whatever way we want.
79+
80+
### Inserting, updating, and delete rows from views
81+
82+
The other querying tools of SQL do not immediately work because they are not real tables. For
83+
example if you try to insert into `ReminderWithList` you will be met with a SQL error:
84+
85+
```swift
86+
ReminderWithList.insert {
87+
ReminderWithList(
88+
reminderTitle: "Morning sync",
89+
remindersListTitle: "Business"
90+
)
91+
}
92+
// 🛑 cannot modify reminderWithLists because it is a view
93+
```
94+
95+
However, it is possible to restore inserts if you can describe how inserting a `(String, String)`
96+
pair into the table ultimately re-routes to inserts into your actual, non-view tables. The logic
97+
for rerouting inserts is highly specific to the situation at hand, and there can be multiple
98+
reasonable ways to do it for a particular view. For example, upon inserting into `ReminderWithList`
99+
we could try first creating a new list with the title, and then use that new list to insert a new
100+
reminder with the title. Or, we could decide that we will not allow creating a new list, and
101+
instead we will just find an existing list with the title, and if we cannot then we fail the query.
102+
103+
In order to demonstrate this technique, we will use the latter rerouting logic: when a
104+
`(String, String)` is inserted into `ReminderWithList` we will only create a new reminder with
105+
the title specified, and we will only find an existing reminders list (if one exists) for the title
106+
specified. And to implement this rerouting logic, one uses a [temporary trigger](<doc:Triggers>) on
107+
the view with an `INSTEAD OF` clause, which allows you to reroute any inserts on the view into some
108+
other table:
109+
110+
```swift
111+
ReminderWithList.createTemporaryTrigger(
112+
insteadOf: .insert { new in
113+
Reminder.insert {
114+
(
115+
$0.title,
116+
$0.remindersListID
117+
)
118+
} values: {
119+
(
120+
new.reminderTitle,
121+
RemindersList
122+
.select(\.id)
123+
.where { $0.title.eq(new.remindersListTitle) }
124+
)
125+
}
126+
}
127+
)
128+
```
129+
130+
After you have installed this trigger into your database you are allowed to insert rows into the
131+
view:
132+
133+
```swift
134+
ReminderWithList.insert {
135+
ReminderWithList(
136+
reminderTitle: "Morning sync",
137+
remindersListTitle: "Business"
138+
)
139+
}
140+
```
141+
142+
Following this pattern you can also restore updates and deletes on the view.
143+
11144
## Topics
12145

13146
### Creating temporary views

Tests/StructuredQueriesMacrosTests/SelectionMacroTests.swift

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,5 +188,187 @@ extension SnapshotTests {
188188
"""
189189
}
190190
}
191+
192+
@Test func primaryKey() {
193+
assertMacro {
194+
"""
195+
@Selection struct Row {
196+
@Column(primaryKey: true)
197+
let id: Int
198+
var title = ""
199+
}
200+
"""
201+
} diagnostics: {
202+
"""
203+
@Selection struct Row {
204+
@Column(primaryKey: true)
205+
┬─────────
206+
╰─ 🛑 '@Selection' primary keys are not supported
207+
✏️ Remove 'primaryKey: true'
208+
let id: Int
209+
var title = ""
210+
}
211+
"""
212+
} fixes: {
213+
"""
214+
@Selection struct Row {
215+
@Column()
216+
let id: Int
217+
var title = ""
218+
}
219+
"""
220+
} expansion: {
221+
"""
222+
struct Row {
223+
let id: Int
224+
var title = ""
225+
226+
public struct Columns: StructuredQueriesCore._SelectedColumns {
227+
public typealias QueryValue = Row
228+
public let selection: [(aliasName: String, expression: StructuredQueriesCore.QueryFragment)]
229+
public init(
230+
id: some StructuredQueriesCore.QueryExpression<Int>,
231+
title: some StructuredQueriesCore.QueryExpression<Swift.String> = StructuredQueriesCore.BindQueryExpression("")
232+
) {
233+
self.selection = [("id", id.queryFragment), ("title", title.queryFragment)]
234+
}
235+
}
236+
}
237+
238+
extension Row: StructuredQueriesCore._Selection {
239+
public init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws {
240+
let id = try decoder.decode(Int.self)
241+
let title = try decoder.decode(Swift.String.self)
242+
guard let id else {
243+
throw QueryDecodingError.missingRequiredColumn
244+
}
245+
guard let title else {
246+
throw QueryDecodingError.missingRequiredColumn
247+
}
248+
self.id = id
249+
self.title = title
250+
}
251+
}
252+
"""
253+
}
254+
}
255+
256+
@Test func tableSelectionPrimaryKey() {
257+
assertMacro {
258+
"""
259+
@Table @Selection struct Row {
260+
@Column(primaryKey: true)
261+
let id: Int
262+
var title = ""
263+
}
264+
"""
265+
} expansion: {
266+
#"""
267+
struct Row {
268+
let id: Int
269+
var title = ""
270+
271+
public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition {
272+
public typealias QueryValue = Row
273+
public let id = StructuredQueriesCore.TableColumn<QueryValue, Int>("id", keyPath: \QueryValue.id)
274+
public let title = StructuredQueriesCore.TableColumn<QueryValue, Swift.String>("title", keyPath: \QueryValue.title, default: "")
275+
public var primaryKey: StructuredQueriesCore.TableColumn<QueryValue, Int> {
276+
self.id
277+
}
278+
public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] {
279+
[QueryValue.columns.id, QueryValue.columns.title]
280+
}
281+
public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] {
282+
[QueryValue.columns.id, QueryValue.columns.title]
283+
}
284+
public var queryFragment: QueryFragment {
285+
"\(self.id), \(self.title)"
286+
}
287+
}
288+
289+
public struct Draft: StructuredQueriesCore.TableDraft {
290+
public typealias PrimaryTable = Row
291+
let id: Int?
292+
var title = ""
293+
public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition {
294+
public typealias QueryValue = Draft
295+
public let id = StructuredQueriesCore.TableColumn<QueryValue, Int?>("id", keyPath: \QueryValue.id)
296+
public let title = StructuredQueriesCore.TableColumn<QueryValue, Swift.String>("title", keyPath: \QueryValue.title, default: "")
297+
public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] {
298+
[QueryValue.columns.id, QueryValue.columns.title]
299+
}
300+
public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] {
301+
[QueryValue.columns.id, QueryValue.columns.title]
302+
}
303+
public var queryFragment: QueryFragment {
304+
"\(self.id), \(self.title)"
305+
}
306+
}
307+
public nonisolated static var columns: TableColumns {
308+
TableColumns()
309+
}
310+
311+
public nonisolated static var tableName: String {
312+
Row.tableName
313+
}
314+
315+
public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws {
316+
self.id = try decoder.decode(Int.self)
317+
self.title = try decoder.decode(Swift.String.self) ?? ""
318+
}
319+
320+
public nonisolated init(_ other: Row) {
321+
self.id = other.id
322+
self.title = other.title
323+
}
324+
public init(
325+
id: Int? = nil,
326+
title: Swift.String = ""
327+
) {
328+
self.id = id
329+
self.title = title
330+
}
331+
}
332+
333+
public struct Columns: StructuredQueriesCore._SelectedColumns {
334+
public typealias QueryValue = Row
335+
public let selection: [(aliasName: String, expression: StructuredQueriesCore.QueryFragment)]
336+
public init(
337+
id: some StructuredQueriesCore.QueryExpression<Int>,
338+
title: some StructuredQueriesCore.QueryExpression<Swift.String> = StructuredQueriesCore.BindQueryExpression("")
339+
) {
340+
self.selection = [("id", id.queryFragment), ("title", title.queryFragment)]
341+
}
342+
}
343+
}
344+
345+
nonisolated extension Row: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement {
346+
public typealias QueryValue = Self
347+
public typealias From = Swift.Never
348+
public nonisolated static var columns: TableColumns {
349+
TableColumns()
350+
}
351+
public nonisolated static var tableName: String {
352+
"rows"
353+
}
354+
}
355+
356+
extension Row: StructuredQueriesCore._Selection {
357+
public init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws {
358+
let id = try decoder.decode(Int.self)
359+
let title = try decoder.decode(Swift.String.self)
360+
guard let id else {
361+
throw QueryDecodingError.missingRequiredColumn
362+
}
363+
guard let title else {
364+
throw QueryDecodingError.missingRequiredColumn
365+
}
366+
self.id = id
367+
self.title = title
368+
}
369+
}
370+
"""#
371+
}
372+
}
191373
}
192374
}

Tests/StructuredQueriesTests/JSONFunctionsTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ extension SnapshotTests {
495495
}
496496

497497
@Selection
498-
private struct ReminderRow: Codable {
498+
private struct ReminderRow {
499499
let assignedUser: User?
500500
let reminder: Reminder
501501
@Column(as: [Tag].JSONRepresentation.self)

0 commit comments

Comments
 (0)