Skip to content

Commit 6f981ee

Browse files
committed
Merge remote-tracking branch 'origin/main' into default-main-actor
2 parents 61feb93 + 5608aaf commit 6f981ee

File tree

12 files changed

+243
-97
lines changed

12 files changed

+243
-97
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jobs:
3434
strategy:
3535
matrix:
3636
swift:
37+
- '6.0'
3738
- '6.1'
3839
runs-on: ubuntu-latest
3940
container: swift:${{ matrix.swift }}

Sources/StructuredQueries/Macros.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,10 @@ public macro sql<QueryValue>(
132132
as queryValueType: QueryValue.Type = QueryValue.self
133133
) -> SQLQueryExpression<QueryValue> =
134134
#externalMacro(module: "StructuredQueriesMacros", type: "SQLMacro")
135+
136+
@freestanding(expression)
137+
public macro sql(
138+
_ queryFragment: QueryFragment,
139+
as queryValueType: Any.Type = Any.self
140+
) -> SQLQueryExpression<Any> =
141+
#externalMacro(module: "StructuredQueriesMacros", type: "SQLMacro")

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,8 @@
2828
2929
- ``jsonArrayLength()``
3030
- ``jsonGroupArray(order:filter:)``
31+
32+
### Optionality
33+
34+
- ``map(_:)``
35+
- ``flatMap(_:)``

Sources/StructuredQueriesCore/Internal/Deprecations.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
11
import Foundation
22

3+
// NB: Deprecated after 0.6.0:
4+
5+
extension QueryFragment {
6+
@available(
7+
*,
8+
deprecated,
9+
message: "Use 'QueryFragment.segments' to build up a SQL string and bindings in a single loop."
10+
)
11+
public var string: String {
12+
segments.reduce(into: "") { string, segment in
13+
switch segment {
14+
case .sql(let sql):
15+
string.append(sql)
16+
case .binding:
17+
string.append("?")
18+
}
19+
}
20+
}
21+
22+
@available(
23+
*,
24+
deprecated,
25+
message: "Use 'QueryFragment.segments' to build up a SQL string and bindings in a single loop."
26+
)
27+
public var bindings: [QueryBinding] {
28+
segments.reduce(into: []) { bindings, segment in
29+
switch segment {
30+
case .sql:
31+
break
32+
case .binding(let binding):
33+
bindings.append(binding)
34+
}
35+
}
36+
}
37+
}
38+
339
// NB: Deprecated after 0.5.1:
440

541
extension Table {
@@ -53,7 +89,7 @@ extension Table {
5389
or conflictResolution: ConflictResolution? = nil,
5490
_ columns: (TableColumns) -> (TableColumn<Self, V1>, repeat TableColumn<Self, each V2>),
5591
select selection: () -> Select<(C1, repeat each C2), From, Joins>,
56-
onConflict updates: ((inout Updates<Self>) -> Void)?,
92+
onConflict updates: ((inout Updates<Self>) -> Void)?
5793
) -> InsertOf<Self>
5894
where C1.QueryValue == V1, (repeat (each C2).QueryValue) == (repeat each V2) {
5995
insert(or: conflictResolution, columns, select: selection, onConflictDoUpdate: updates)

Sources/StructuredQueriesCore/Internal/PrettyPrinting.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ extension QueryFragment {
3030
#if DEBUG
3131
guard isTesting else { return self }
3232
var query = self
33-
query.string = " \(query.string.replacingOccurrences(of: "\n", with: "\n "))"
33+
query.segments.insert(.sql(" "), at: 0)
34+
for index in query.segments.indices {
35+
switch query.segments[index] {
36+
case .sql(let sql):
37+
query.segments[index] = .sql(sql.replacingOccurrences(of: "\n", with: "\n "))
38+
case .binding:
39+
continue
40+
}
41+
}
3442
return query
3543
#else
3644
return self

Sources/StructuredQueriesCore/Optional.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,43 @@ where Wrapped.TableColumns: PrimaryKeyedTableDefinition {
137137
self[dynamicMember: \.primaryKey]
138138
}
139139
}
140+
141+
extension QueryExpression where QueryValue: _OptionalProtocol {
142+
/// Creates and optionalizes a new expression from this one by applying an unwrapped version of
143+
/// this expression to a given closure.
144+
///
145+
/// ```swift
146+
/// Reminder.where {
147+
/// $0.dueDate.map { $0 > Date() }
148+
/// }
149+
/// // SELECT … FROM "reminders"
150+
/// // WHERE "reminders"."dueDate" > '2018-01-29 00:08:00.000'
151+
/// ```
152+
///
153+
/// - Parameter transform: A closure that takes an unwrapped version of this expression.
154+
/// - Returns: The result of the transform function, optionalized.
155+
public func map<T>(
156+
_ transform: (SQLQueryExpression<QueryValue.Wrapped>) -> some QueryExpression<T>
157+
) -> some QueryExpression<T?> {
158+
SQLQueryExpression(transform(SQLQueryExpression(queryFragment)).queryFragment)
159+
}
160+
161+
/// Creates a new optional expression from this one by applying an unwrapped version of this
162+
/// expression to a given closure.
163+
///
164+
/// ```swift
165+
/// Reminder.select {
166+
/// $0.dueDate.flatMap { $0.max() }
167+
/// }
168+
/// // SELECT max("reminders"."dueDate") FROM "reminders"
169+
/// // => [Date?]
170+
/// ```
171+
///
172+
/// - Parameter transform: A closure that takes an unwrapped version of this expression.
173+
/// - Returns: The result of the transform function.
174+
public func flatMap<T>(
175+
_ transform: (SQLQueryExpression<QueryValue.Wrapped>) -> some QueryExpression<T?>
176+
) -> some QueryExpression<T?> {
177+
SQLQueryExpression(transform(SQLQueryExpression(queryFragment)).queryFragment)
178+
}
179+
}

Sources/StructuredQueriesCore/QueryFragment.swift

Lines changed: 61 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,44 @@ import StructuredQueriesSupport
55
/// You will typically create instances of this type using string literals, where bindings are
66
/// directly interpolated into the string. This most commonly occurs when using the `#sql` macro,
77
/// which takes values of this type.
8-
public struct QueryFragment: Hashable, Sendable, CustomDebugStringConvertible {
9-
#if DEBUG
10-
/// The underlying SQL string.
11-
public var string: String
12-
#else
13-
/// The underlying SQL string.
14-
public package(set) var string: String
15-
#endif
8+
public struct QueryFragment: Hashable, Sendable {
9+
/// A segment of a query fragment.
10+
public enum Segment: Hashable, Sendable {
11+
/// A raw SQL fragment.
12+
case sql(String)
1613

17-
#if DEBUG
18-
/// An array of parameterized statement bindings.
19-
public var bindings: [QueryBinding]
20-
#else
21-
/// An array of parameterized statement bindings.
22-
public package(set) var bindings: [QueryBinding]
23-
#endif
14+
/// A binding.
15+
case binding(QueryBinding)
16+
}
17+
18+
/// An array of segments backing this query fragment.
19+
public internal(set) var segments: [Segment] = []
20+
21+
fileprivate init(segments: [Segment]) {
22+
self.segments = segments
23+
}
2424

25-
init(_ string: String = "", _ bindings: [QueryBinding] = []) {
26-
self.string = string
27-
self.bindings = bindings
25+
init(_ string: String = "") {
26+
self.init(segments: [.sql(string)])
2827
}
2928

3029
/// A Boolean value indicating whether the query fragment is empty.
3130
public var isEmpty: Bool {
32-
return string.isEmpty && bindings.isEmpty
31+
segments.allSatisfy {
32+
switch $0 {
33+
case .sql(let sql):
34+
sql.isEmpty
35+
case .binding:
36+
false
37+
}
38+
}
3339
}
3440

3541
/// Appends the given fragment to this query fragment.
3642
///
3743
/// - Parameter other: Another query fragment.
3844
public mutating func append(_ other: Self) {
39-
string.append(other.string)
40-
bindings.append(contentsOf: other.bindings)
45+
segments.append(contentsOf: other.segments)
4146
}
4247

4348
/// Appends a given query fragment to another fragment.
@@ -52,35 +57,35 @@ public struct QueryFragment: Hashable, Sendable, CustomDebugStringConvertible {
5257
return query
5358
}
5459

60+
/// Returns a prepared SQL string and associated bindings for this query.
61+
///
62+
/// - Parameter template: Prepare a template string for a binding at a given 1-based offset.
63+
/// - Returns: A SQL string and array of associated bindings.
64+
public func prepare(
65+
_ template: (_ offset: Int) -> String
66+
) -> (sql: String, bindings: [QueryBinding]) {
67+
segments.enumerated().reduce(into: (sql: "", bindings: [QueryBinding]())) {
68+
switch $1.element {
69+
case .sql(let sql):
70+
$0.sql.append(sql)
71+
case .binding(let binding):
72+
$0.sql.append(template($1.offset + 1))
73+
$0.bindings.append(binding)
74+
}
75+
}
76+
}
77+
}
78+
79+
extension QueryFragment: CustomDebugStringConvertible {
5580
public var debugDescription: String {
56-
var compiled = ""
57-
var bindings = bindings
58-
var currentDelimiter: Character?
59-
compiled.reserveCapacity(string.count)
60-
let delimiters: [Character: Character] = [
61-
#"""#: #"""#,
62-
"'": "'",
63-
"`": "`",
64-
"[": "]",
65-
]
66-
for character in string {
67-
if let delimiter = currentDelimiter {
68-
if delimiter == character,
69-
compiled.last != character || compiled.last == delimiters[delimiter]
70-
{
71-
currentDelimiter = nil
72-
}
73-
compiled.append(character)
74-
} else if delimiters.keys.contains(character) {
75-
currentDelimiter = character
76-
compiled.append(character)
77-
} else if character == "?" {
78-
compiled.append(bindings.removeFirst().debugDescription)
79-
} else {
80-
compiled.append(character)
81+
segments.reduce(into: "") { debugDescription, segment in
82+
switch segment {
83+
case .sql(let sql):
84+
debugDescription.append(sql)
85+
case .binding(let binding):
86+
debugDescription.append(binding.debugDescription)
8187
}
8288
}
83-
return compiled
8489
}
8590
}
8691

@@ -103,7 +108,7 @@ extension [QueryFragment] {
103108

104109
extension QueryFragment: ExpressibleByStringInterpolation {
105110
public init(stringInterpolation: StringInterpolation) {
106-
self.init(stringInterpolation.string, stringInterpolation.bindings)
111+
self.init(segments: stringInterpolation.segments)
107112
}
108113

109114
public init(stringLiteral value: String) {
@@ -132,16 +137,14 @@ extension QueryFragment: ExpressibleByStringInterpolation {
132137
}
133138

134139
public struct StringInterpolation: StringInterpolationProtocol {
135-
public var string = ""
136-
public var bindings: [QueryBinding] = []
140+
fileprivate var segments: [Segment] = []
137141

138142
public init(literalCapacity: Int, interpolationCount: Int) {
139-
string.reserveCapacity(literalCapacity)
140-
bindings.reserveCapacity(interpolationCount)
143+
segments.reserveCapacity(interpolationCount)
141144
}
142145

143146
public mutating func appendLiteral(_ literal: String) {
144-
string.append(literal)
147+
segments.append(.sql(literal))
145148
}
146149

147150
/// Append a quoted fragment to the interpolation.
@@ -162,7 +165,7 @@ extension QueryFragment: ExpressibleByStringInterpolation {
162165
quote sql: String,
163166
delimiter: QuoteDelimiter = .identifier
164167
) {
165-
string.append(sql.quoted(delimiter))
168+
segments.append(.sql(sql.quoted(delimiter)))
166169
}
167170

168171
/// Append a raw SQL string to the interpolation.
@@ -174,7 +177,7 @@ extension QueryFragment: ExpressibleByStringInterpolation {
174177
///
175178
/// - Parameter sql: A raw query string.
176179
public mutating func appendInterpolation(raw sql: String) {
177-
string.append(sql)
180+
segments.append(.sql(sql))
178181
}
179182

180183
/// Append a raw lossless string to the interpolation.
@@ -191,23 +194,21 @@ extension QueryFragment: ExpressibleByStringInterpolation {
191194
///
192195
/// - Parameter sql: A raw query string.
193196
public mutating func appendInterpolation(raw sql: some LosslessStringConvertible) {
194-
string.append(sql.description)
197+
segments.append(.sql(sql.description))
195198
}
196199

197200
/// Append a query binding to the interpolation.
198201
///
199202
/// - Parameter binding: A query binding.
200203
public mutating func appendInterpolation(_ binding: QueryBinding) {
201-
string.append("?")
202-
bindings.append(binding)
204+
segments.append(.binding(binding))
203205
}
204206

205207
/// Append a query fragment to the interpolation.
206208
///
207209
/// - Parameter fragment: A query fragment.
208210
public mutating func appendInterpolation(_ fragment: QueryFragment) {
209-
string.append(fragment.string)
210-
bindings.append(contentsOf: fragment.bindings)
211+
segments.append(contentsOf: fragment.segments)
211212
}
212213

213214
/// Append a query expression to the interpolation.

0 commit comments

Comments
 (0)