Skip to content

Commit f231776

Browse files
authored
Binding support in createTemporaryView (#215)
* Binding support in `createTemporaryView` Create view/trigger statements do not allow binds, but our query builder binds values by default. For triggers we "compile" the binds into the query string directly, so we should do the same in views. * wip * wip * wip
1 parent 0352774 commit f231776

File tree

5 files changed

+143
-74
lines changed

5 files changed

+143
-74
lines changed

Sources/StructuredQueriesCore/QueryFragment.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import Foundation
2+
import IssueReporting
3+
14
/// A type representing a SQL string and its bindings.
25
///
36
/// You will typically create instances of this type using string literals, where bindings are
@@ -145,6 +148,69 @@ extension QueryFragment: ExpressibleByStringInterpolation {
145148
self.init(sql.quoted(delimiter))
146149
}
147150

151+
package func compiled(statementType: String) -> Self {
152+
segments.reduce(into: QueryFragment()) {
153+
switch $1 {
154+
case .sql(let sql):
155+
$0.append("\(raw: sql)")
156+
case .binding(let binding):
157+
switch binding {
158+
case .blob(let blob):
159+
let hex = blob.reduce(into: "") {
160+
let hex = String($1, radix: 16)
161+
if hex.count == 1 {
162+
$0.append("0")
163+
}
164+
$0.append(hex)
165+
}
166+
$0.append("X\(quote: hex, delimiter: .text)")
167+
case .bool(let bool):
168+
$0.append("\(raw: bool ? 1 : 0)")
169+
case .double(let double):
170+
$0.append("\(raw: double)")
171+
case .date(let date):
172+
reportIssue(
173+
"""
174+
Swift Date values should not be bound to a '\(statementType)' statement. Specify dates \
175+
using the '#sql' macro, instead. For example, the current date:
176+
177+
#sql("datetime()")
178+
179+
Or a constant date:
180+
181+
#sql("'2018-01-29 00:08:00'")
182+
"""
183+
)
184+
$0.append("\(quote: date.iso8601String, delimiter: .text)")
185+
case .int(let int):
186+
$0.append("\(raw: int)")
187+
case .null:
188+
$0.append("NULL")
189+
case .text(let string):
190+
$0.append("\(quote: string, delimiter: .text)")
191+
case .uint(let uint):
192+
$0.append("\(raw: uint)")
193+
case .uuid(let uuid):
194+
reportIssue(
195+
"""
196+
Swift UUID values should not be bound to a '\(statementType)' statement. Specify UUIDs \
197+
using the '#sql' macro, instead. For example, a random UUID:
198+
199+
#sql("uuid()")
200+
201+
Or a constant UUID:
202+
203+
#sql("'00000000-0000-0000-0000-000000000000'")
204+
"""
205+
)
206+
$0.append("\(quote: uuid.uuidString.lowercased(), delimiter: .text)")
207+
case .invalid(let error):
208+
$0.append("\(.invalid(error.underlyingError))")
209+
}
210+
}
211+
}
212+
}
213+
148214
public struct StringInterpolation: StringInterpolationProtocol {
149215
fileprivate var segments: [Segment] = []
150216

Sources/StructuredQueriesSQLiteCore/Triggers.swift

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import Foundation
2-
import IssueReporting
32

43
extension Table {
54
/// A `CREATE TEMPORARY TRIGGER` statement that executes after a database event.
@@ -425,72 +424,7 @@ public struct TemporaryTrigger<On: Table>: Sendable, Statement {
425424
}
426425
query.append("\(.newlineOrSpace)\(triggerName.indented())")
427426
query.append("\(.newlineOrSpace)\(when.rawValue) \(operation)")
428-
return query.segments.reduce(into: QueryFragment()) {
429-
switch $1 {
430-
case .sql(let sql):
431-
$0.append("\(raw: sql)")
432-
case .binding(let binding):
433-
switch binding {
434-
case .blob(let blob):
435-
reportIssue(
436-
"""
437-
Cannot bind bytes to a trigger statement. To hardcode a constant BLOB, use the '#sql' \
438-
macro.
439-
"""
440-
)
441-
let hex = blob.reduce(into: "") {
442-
let hex = String($1, radix: 16)
443-
if hex.count == 1 {
444-
$0.append("0")
445-
}
446-
$0.append(hex)
447-
}
448-
$0.append("unhex(\(quote: hex, delimiter: .text))")
449-
case .bool(let bool):
450-
$0.append("\(raw: bool ? 1 : 0)")
451-
case .double(let double):
452-
$0.append("\(raw: double)")
453-
case .date(let date):
454-
reportIssue(
455-
"""
456-
Cannot bind a date to a trigger statement. Specify dates using the '#sql' macro, \
457-
instead. For example, the current date:
458-
459-
#sql("datetime()")
460-
461-
Or a constant date:
462-
463-
#sql("'2018-01-29 00:08:00'")
464-
"""
465-
)
466-
$0.append("\(quote: date.iso8601String, delimiter: .text)")
467-
case .int(let int):
468-
$0.append("\(raw: int)")
469-
case .null:
470-
$0.append("NULL")
471-
case .text(let string):
472-
$0.append("\(quote: string, delimiter: .text)")
473-
case .uint(let uint):
474-
$0.append("\(raw: uint)")
475-
case .uuid(let uuid):
476-
reportIssue(
477-
"""
478-
Cannot bind a UUID to a trigger statement. Specify UUIDs using the '#sql' macro, \
479-
instead. For example, a random UUID:
480-
481-
#sql("uuid()")
482-
483-
Or a constant UUID:
484-
485-
#sql("'00000000-0000-0000-0000-000000000000'")
486-
"""
487-
)
488-
$0.append("\(quote: uuid.uuidString.lowercased(), delimiter: .text)")
489-
case .invalid(let error):
490-
$0.append("\(.invalid(error.underlyingError))")
491-
}
492-
}
493-
}
427+
return query.compiled(statementType: "CREATE TEMPORARY TRIGGER")
494428
}
495429

496430
private var triggerName: QueryFragment {

Sources/StructuredQueriesSQLiteCore/Views.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@ where Selection.QueryValue == View {
6161
query.append("\(.newlineOrSpace)(\(columnNames.joined(separator: ", ")))")
6262
query.append("\(.newlineOrSpace)AS")
6363
query.append("\(.newlineOrSpace)\(select)")
64-
return query
64+
return query.compiled(statementType: "CREATE TEMPORARY VIEW")
6565
}
6666
}

Tests/StructuredQueriesTests/TriggersTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ extension SnapshotTests {
6868
} matching: {
6969
$0.description.contains(
7070
"""
71-
Cannot bind a date to a trigger statement. Specify dates using the '#sql' macro, \
72-
instead. For example, the current date:
71+
Swift Date values should not be bound to a 'CREATE TEMPORARY TRIGGER' statement. Specify \
72+
dates using the '#sql' macro, instead. For example, the current date:
7373
7474
#sql("datetime()")
7575

Tests/StructuredQueriesTests/ViewsTests.swift

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,6 @@ extension SnapshotTests {
213213
("reminderID", "reminderTitle", "remindersListTitle")
214214
VALUES
215215
(NULL, 'Morning sync', 'Business')
216-
"""
217-
} results: {
218-
"""
219-
220216
"""
221217
}
222218

@@ -290,6 +286,73 @@ extension SnapshotTests {
290286
"""
291287
}
292288
}
289+
290+
@Test func viewWithBindings() {
291+
assertQuery(
292+
PastDueReminder.createTemporaryView(
293+
as:
294+
Reminder.where(\.isPastDue)
295+
.select {
296+
PastDueReminder.Columns(
297+
reminderID: $0.id,
298+
title: $0.title
299+
)
300+
}
301+
)
302+
) {
303+
"""
304+
CREATE TEMPORARY VIEW
305+
"pastDueReminders"
306+
("reminderID", "title")
307+
AS
308+
SELECT "reminders"."id" AS "reminderID", "reminders"."title" AS "title"
309+
FROM "reminders"
310+
WHERE (NOT ("reminders"."isCompleted")) AND (coalesce("reminders"."dueDate", date('now')) < date('now'))
311+
"""
312+
}
313+
assertQuery(
314+
PastDueReminder.all
315+
) {
316+
"""
317+
SELECT "pastDueReminders"."reminderID", "pastDueReminders"."title"
318+
FROM "pastDueReminders"
319+
"""
320+
} results: {
321+
"""
322+
┌─────────────────────────────────────┐
323+
│ PastDueReminder( │
324+
│ reminderID: 1, │
325+
│ title: "Groceries"
326+
│ ) │
327+
├─────────────────────────────────────┤
328+
│ PastDueReminder( │
329+
│ reminderID: 2, │
330+
│ title: "Haircut"
331+
│ ) │
332+
├─────────────────────────────────────┤
333+
│ PastDueReminder( │
334+
│ reminderID: 3, │
335+
│ title: "Doctor appointment"
336+
│ ) │
337+
├─────────────────────────────────────┤
338+
│ PastDueReminder( │
339+
│ reminderID: 6, │
340+
│ title: "Pick up kids from school"
341+
│ ) │
342+
├─────────────────────────────────────┤
343+
│ PastDueReminder( │
344+
│ reminderID: 8, │
345+
│ title: "Take out trash"
346+
│ ) │
347+
├─────────────────────────────────────┤
348+
│ PastDueReminder( │
349+
│ reminderID: 9, │
350+
│ title: "Call accountant"
351+
│ ) │
352+
└─────────────────────────────────────┘
353+
"""
354+
}
355+
}
293356
}
294357
}
295358

@@ -299,6 +362,12 @@ private struct CompletedReminder {
299362
let title: String
300363
}
301364

365+
@Table
366+
private struct PastDueReminder {
367+
let reminderID: Reminder.ID
368+
let title: String
369+
}
370+
302371
@Table
303372
private struct ReminderWithList {
304373
@Column(primaryKey: true)

0 commit comments

Comments
 (0)