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
66 changes: 66 additions & 0 deletions Sources/StructuredQueriesCore/QueryFragment.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import Foundation
import IssueReporting

/// A type representing a SQL string and its bindings.
///
/// You will typically create instances of this type using string literals, where bindings are
Expand Down Expand Up @@ -145,6 +148,69 @@ extension QueryFragment: ExpressibleByStringInterpolation {
self.init(sql.quoted(delimiter))
}

package func compiled(statementType: String) -> Self {
segments.reduce(into: QueryFragment()) {
switch $1 {
case .sql(let sql):
$0.append("\(raw: sql)")
case .binding(let binding):
switch binding {
case .blob(let blob):
let hex = blob.reduce(into: "") {
let hex = String($1, radix: 16)
if hex.count == 1 {
$0.append("0")
}
$0.append(hex)
}
$0.append("X\(quote: hex, delimiter: .text)")
case .bool(let bool):
$0.append("\(raw: bool ? 1 : 0)")
case .double(let double):
$0.append("\(raw: double)")
case .date(let date):
reportIssue(
"""
Swift Date values should not be bound to a '\(statementType)' statement. Specify dates \
using the '#sql' macro, instead. For example, the current date:

#sql("datetime()")

Or a constant date:

#sql("'2018-01-29 00:08:00'")
"""
)
$0.append("\(quote: date.iso8601String, delimiter: .text)")
case .int(let int):
$0.append("\(raw: int)")
case .null:
$0.append("NULL")
case .text(let string):
$0.append("\(quote: string, delimiter: .text)")
case .uint(let uint):
$0.append("\(raw: uint)")
case .uuid(let uuid):
reportIssue(
"""
Swift UUID values should not be bound to a '\(statementType)' statement. Specify UUIDs \
using the '#sql' macro, instead. For example, a random UUID:

#sql("uuid()")

Or a constant UUID:

#sql("'00000000-0000-0000-0000-000000000000'")
"""
)
$0.append("\(quote: uuid.uuidString.lowercased(), delimiter: .text)")
case .invalid(let error):
$0.append("\(.invalid(error.underlyingError))")
}
}
}
}

public struct StringInterpolation: StringInterpolationProtocol {
fileprivate var segments: [Segment] = []

Expand Down
68 changes: 1 addition & 67 deletions Sources/StructuredQueriesSQLiteCore/Triggers.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import IssueReporting

extension Table {
/// A `CREATE TEMPORARY TRIGGER` statement that executes after a database event.
Expand Down Expand Up @@ -425,72 +424,7 @@ public struct TemporaryTrigger<On: Table>: Sendable, Statement {
}
query.append("\(.newlineOrSpace)\(triggerName.indented())")
query.append("\(.newlineOrSpace)\(when.rawValue) \(operation)")
return query.segments.reduce(into: QueryFragment()) {
switch $1 {
case .sql(let sql):
$0.append("\(raw: sql)")
case .binding(let binding):
switch binding {
case .blob(let blob):
reportIssue(
"""
Cannot bind bytes to a trigger statement. To hardcode a constant BLOB, use the '#sql' \
macro.
"""
)
let hex = blob.reduce(into: "") {
let hex = String($1, radix: 16)
if hex.count == 1 {
$0.append("0")
}
$0.append(hex)
}
$0.append("unhex(\(quote: hex, delimiter: .text))")
case .bool(let bool):
$0.append("\(raw: bool ? 1 : 0)")
case .double(let double):
$0.append("\(raw: double)")
case .date(let date):
reportIssue(
"""
Cannot bind a date to a trigger statement. Specify dates using the '#sql' macro, \
instead. For example, the current date:

#sql("datetime()")

Or a constant date:

#sql("'2018-01-29 00:08:00'")
"""
)
$0.append("\(quote: date.iso8601String, delimiter: .text)")
case .int(let int):
$0.append("\(raw: int)")
case .null:
$0.append("NULL")
case .text(let string):
$0.append("\(quote: string, delimiter: .text)")
case .uint(let uint):
$0.append("\(raw: uint)")
case .uuid(let uuid):
reportIssue(
"""
Cannot bind a UUID to a trigger statement. Specify UUIDs using the '#sql' macro, \
instead. For example, a random UUID:

#sql("uuid()")

Or a constant UUID:

#sql("'00000000-0000-0000-0000-000000000000'")
"""
)
$0.append("\(quote: uuid.uuidString.lowercased(), delimiter: .text)")
case .invalid(let error):
$0.append("\(.invalid(error.underlyingError))")
}
}
}
return query.compiled(statementType: "CREATE TEMPORARY TRIGGER")
}

private var triggerName: QueryFragment {
Expand Down
2 changes: 1 addition & 1 deletion Sources/StructuredQueriesSQLiteCore/Views.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ where Selection.QueryValue == View {
query.append("\(.newlineOrSpace)(\(columnNames.joined(separator: ", ")))")
query.append("\(.newlineOrSpace)AS")
query.append("\(.newlineOrSpace)\(select)")
return query
return query.compiled(statementType: "CREATE TEMPORARY VIEW")
}
}
4 changes: 2 additions & 2 deletions Tests/StructuredQueriesTests/TriggersTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ extension SnapshotTests {
} matching: {
$0.description.contains(
"""
Cannot bind a date to a trigger statement. Specify dates using the '#sql' macro, \
instead. For example, the current date:
Swift Date values should not be bound to a 'CREATE TEMPORARY TRIGGER' statement. Specify \
dates using the '#sql' macro, instead. For example, the current date:

#sql("datetime()")

Expand Down
77 changes: 73 additions & 4 deletions Tests/StructuredQueriesTests/ViewsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,6 @@ extension SnapshotTests {
("reminderID", "reminderTitle", "remindersListTitle")
VALUES
(NULL, 'Morning sync', 'Business')
"""
} results: {
"""

"""
}

Expand Down Expand Up @@ -290,6 +286,73 @@ extension SnapshotTests {
"""
}
}

@Test func viewWithBindings() {
assertQuery(
PastDueReminder.createTemporaryView(
as:
Reminder.where(\.isPastDue)
.select {
PastDueReminder.Columns(
reminderID: $0.id,
title: $0.title
)
}
)
) {
"""
CREATE TEMPORARY VIEW
"pastDueReminders"
("reminderID", "title")
AS
SELECT "reminders"."id" AS "reminderID", "reminders"."title" AS "title"
FROM "reminders"
WHERE (NOT ("reminders"."isCompleted")) AND (coalesce("reminders"."dueDate", date('now')) < date('now'))
"""
}
assertQuery(
PastDueReminder.all
) {
"""
SELECT "pastDueReminders"."reminderID", "pastDueReminders"."title"
FROM "pastDueReminders"
"""
} results: {
"""
┌─────────────────────────────────────┐
│ PastDueReminder( │
│ reminderID: 1, │
│ title: "Groceries" │
│ ) │
├─────────────────────────────────────┤
│ PastDueReminder( │
│ reminderID: 2, │
│ title: "Haircut" │
│ ) │
├─────────────────────────────────────┤
│ PastDueReminder( │
│ reminderID: 3, │
│ title: "Doctor appointment" │
│ ) │
├─────────────────────────────────────┤
│ PastDueReminder( │
│ reminderID: 6, │
│ title: "Pick up kids from school" │
│ ) │
├─────────────────────────────────────┤
│ PastDueReminder( │
│ reminderID: 8, │
│ title: "Take out trash" │
│ ) │
├─────────────────────────────────────┤
│ PastDueReminder( │
│ reminderID: 9, │
│ title: "Call accountant" │
│ ) │
└─────────────────────────────────────┘
"""
}
}
}
}

Expand All @@ -299,6 +362,12 @@ private struct CompletedReminder {
let title: String
}

@Table
private struct PastDueReminder {
let reminderID: Reminder.ID
let title: String
}

@Table
private struct ReminderWithList {
@Column(primaryKey: true)
Expand Down