Skip to content

Commit 4044506

Browse files
committed
Begin to decouple query fragments from SQLite
Currently, query fragments are modeled as strings with `?`s denoting bindings, and an array of bindings. Instead we should model things as a single array of segments that can be concatenated with whatever form of binding identifier we need. For example, Postgres can use `$1`, `$2`, etc. This remodel also has the positive effect of safer debug descriptions of our queries.
1 parent 22c6e7d commit 4044506

File tree

5 files changed

+100
-65
lines changed

5 files changed

+100
-65
lines changed

Sources/StructuredQueriesCore/Internal/Deprecations.swift

Lines changed: 36 additions & 0 deletions
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 {

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/QueryFragment.swift

Lines changed: 34 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,34 @@ 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+
// TODO: Call this 'Element' and make 'QueryFragment' a collection of them?
10+
public enum Segment: Hashable, Sendable {
11+
case sql(String)
12+
case binding(QueryBinding)
13+
}
14+
15+
// TODO: Make 'private(set)' and add APIs to support extensibility like 'indent()'?
16+
public internal(set) var segments: [Segment] = []
1617

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
18+
fileprivate init(segments: [Segment]) {
19+
self.segments = segments
20+
}
2421

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

3026
/// A Boolean value indicating whether the query fragment is empty.
3127
public var isEmpty: Bool {
32-
return string.isEmpty && bindings.isEmpty
28+
segments.isEmpty
3329
}
3430

3531
/// Appends the given fragment to this query fragment.
3632
///
3733
/// - Parameter other: Another query fragment.
3834
public mutating func append(_ other: Self) {
39-
string.append(other.string)
40-
bindings.append(contentsOf: other.bindings)
35+
segments.append(contentsOf: other.segments)
4136
}
4237

4338
/// Appends a given query fragment to another fragment.
@@ -51,36 +46,18 @@ public struct QueryFragment: Hashable, Sendable, CustomDebugStringConvertible {
5146
query += rhs
5247
return query
5348
}
49+
}
5450

51+
extension QueryFragment: CustomDebugStringConvertible {
5552
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)
53+
segments.reduce(into: "") { debugDescription, segment in
54+
switch segment {
55+
case .sql(let sql):
56+
debugDescription.append(sql)
57+
case .binding(let binding):
58+
debugDescription.append(binding.debugDescription)
8159
}
8260
}
83-
return compiled
8461
}
8562
}
8663

@@ -103,7 +80,7 @@ extension [QueryFragment] {
10380

10481
extension QueryFragment: ExpressibleByStringInterpolation {
10582
public init(stringInterpolation: StringInterpolation) {
106-
self.init(stringInterpolation.string, stringInterpolation.bindings)
83+
self.init(segments: stringInterpolation.segments)
10784
}
10885

10986
public init(stringLiteral value: String) {
@@ -132,16 +109,15 @@ extension QueryFragment: ExpressibleByStringInterpolation {
132109
}
133110

134111
public struct StringInterpolation: StringInterpolationProtocol {
135-
public var string = ""
136-
public var bindings: [QueryBinding] = []
112+
fileprivate var segments: [Segment] = []
137113

138114
public init(literalCapacity: Int, interpolationCount: Int) {
139-
string.reserveCapacity(literalCapacity)
140-
bindings.reserveCapacity(interpolationCount)
115+
// TODO: Should all the segments' strings share the same contiguous storage as substring/span?
116+
segments.reserveCapacity(interpolationCount)
141117
}
142118

143119
public mutating func appendLiteral(_ literal: String) {
144-
string.append(literal)
120+
segments.append(.sql(literal))
145121
}
146122

147123
/// Append a quoted fragment to the interpolation.
@@ -162,7 +138,7 @@ extension QueryFragment: ExpressibleByStringInterpolation {
162138
quote sql: String,
163139
delimiter: QuoteDelimiter = .identifier
164140
) {
165-
string.append(sql.quoted(delimiter))
141+
segments.append(.sql(sql.quoted(delimiter)))
166142
}
167143

168144
/// Append a raw SQL string to the interpolation.
@@ -174,7 +150,7 @@ extension QueryFragment: ExpressibleByStringInterpolation {
174150
///
175151
/// - Parameter sql: A raw query string.
176152
public mutating func appendInterpolation(raw sql: String) {
177-
string.append(sql)
153+
segments.append(.sql(sql))
178154
}
179155

180156
/// Append a raw lossless string to the interpolation.
@@ -191,23 +167,21 @@ extension QueryFragment: ExpressibleByStringInterpolation {
191167
///
192168
/// - Parameter sql: A raw query string.
193169
public mutating func appendInterpolation(raw sql: some LosslessStringConvertible) {
194-
string.append(sql.description)
170+
segments.append(.sql(sql.description))
195171
}
196172

197173
/// Append a query binding to the interpolation.
198174
///
199175
/// - Parameter binding: A query binding.
200176
public mutating func appendInterpolation(_ binding: QueryBinding) {
201-
string.append("?")
202-
bindings.append(binding)
177+
segments.append(.binding(binding))
203178
}
204179

205180
/// Append a query fragment to the interpolation.
206181
///
207182
/// - Parameter fragment: A query fragment.
208183
public mutating func appendInterpolation(_ fragment: QueryFragment) {
209-
string.append(fragment.string)
210-
bindings.append(contentsOf: fragment.bindings)
184+
segments.append(contentsOf: fragment.segments)
211185
}
212186

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

Sources/StructuredQueriesCore/TableAlias.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,16 @@ extension QueryFragment {
223223
of _: T.Type, with _: A.Type
224224
) -> QueryFragment {
225225
var query = self
226-
query.string = query.string
227-
.replacingOccurrences(of: T.tableName.quoted(), with: A.aliasName.quoted())
226+
for index in query.segments.indices {
227+
switch query.segments[index] {
228+
case .sql(let sql):
229+
query.segments[index] = .sql(
230+
sql.replacingOccurrences(of: T.tableName.quoted(), with: A.aliasName.quoted())
231+
)
232+
case .binding:
233+
continue
234+
}
235+
}
228236
return query
229237
}
230238
}

Sources/StructuredQueriesSQLite/Database.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,21 @@ public struct Database {
127127
func withStatement<R>(
128128
_ query: QueryFragment, body: (OpaquePointer) throws -> R
129129
) throws -> R {
130+
let (sql, bindings) = query.segments.reduce(into: (sql: "", bindings: [QueryBinding]())) {
131+
switch $1 {
132+
case .sql(let sql):
133+
$0.sql.append(sql)
134+
case .binding(let binding):
135+
$0.sql.append("?")
136+
$0.bindings.append(binding)
137+
}
138+
}
130139
var statement: OpaquePointer?
131-
let code = sqlite3_prepare_v2(storage.handle, query.string, -1, &statement, nil)
140+
let code = sqlite3_prepare_v2(storage.handle, sql, -1, &statement, nil)
132141
guard code == SQLITE_OK, let statement
133142
else { throw SQLiteError(db: storage.handle) }
134143
defer { sqlite3_finalize(statement) }
135-
for (index, binding) in zip(Int32(1)..., query.bindings) {
144+
for (index, binding) in zip(Int32(1)..., bindings) {
136145
let result =
137146
switch binding {
138147
case .blob(let blob):

0 commit comments

Comments
 (0)