Skip to content

Commit 5608aaf

Browse files
authored
Begin to decouple query fragments from SQLite (#78)
* 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. * wip * wip * wip
1 parent 073c8ae commit 5608aaf

File tree

5 files changed

+119
-65
lines changed

5 files changed

+119
-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: 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.

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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,13 @@ public struct Database {
127127
func withStatement<R>(
128128
_ query: QueryFragment, body: (OpaquePointer) throws -> R
129129
) throws -> R {
130+
let (sql, bindings) = query.prepare { _ in "?" }
130131
var statement: OpaquePointer?
131-
let code = sqlite3_prepare_v2(storage.handle, query.string, -1, &statement, nil)
132+
let code = sqlite3_prepare_v2(storage.handle, sql, -1, &statement, nil)
132133
guard code == SQLITE_OK, let statement
133134
else { throw SQLiteError(db: storage.handle) }
134135
defer { sqlite3_finalize(statement) }
135-
for (index, binding) in zip(Int32(1)..., query.bindings) {
136+
for (index, binding) in zip(Int32(1)..., bindings) {
136137
let result =
137138
switch binding {
138139
case .blob(let blob):

0 commit comments

Comments
 (0)