Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
36 changes: 36 additions & 0 deletions Sources/StructuredQueriesCore/Internal/Deprecations.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
import Foundation

// NB: Deprecated after 0.6.0:

extension QueryFragment {
@available(
*,
deprecated,
message: "Use 'QueryFragment.segments' to build up a SQL string and bindings in a single loop."
)
public var string: String {
segments.reduce(into: "") { string, segment in
switch segment {
case .sql(let sql):
string.append(sql)
case .binding:
string.append("?")
}
}
}

@available(
*,
deprecated,
message: "Use 'QueryFragment.segments' to build up a SQL string and bindings in a single loop."
)
public var bindings: [QueryBinding] {
segments.reduce(into: []) { bindings, segment in
switch segment {
case .sql:
break
case .binding(let binding):
bindings.append(binding)
}
}
}
}

// NB: Deprecated after 0.5.1:

extension Table {
Expand Down
10 changes: 9 additions & 1 deletion Sources/StructuredQueriesCore/Internal/PrettyPrinting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@ extension QueryFragment {
#if DEBUG
guard isTesting else { return self }
var query = self
query.string = " \(query.string.replacingOccurrences(of: "\n", with: "\n "))"
query.segments.insert(.sql(" "), at: 0)
for index in query.segments.indices {
switch query.segments[index] {
case .sql(let sql):
query.segments[index] = .sql(sql.replacingOccurrences(of: "\n", with: "\n "))
case .binding:
continue
}
}
return query
#else
return self
Expand Down
101 changes: 41 additions & 60 deletions Sources/StructuredQueriesCore/QueryFragment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,41 @@ import StructuredQueriesSupport
/// You will typically create instances of this type using string literals, where bindings are
/// directly interpolated into the string. This most commonly occurs when using the `#sql` macro,
/// which takes values of this type.
public struct QueryFragment: Hashable, Sendable, CustomDebugStringConvertible {
#if DEBUG
/// The underlying SQL string.
public var string: String
#else
/// The underlying SQL string.
public package(set) var string: String
#endif
public struct QueryFragment: Hashable, Sendable {
// TODO: Call this 'Element' and make 'QueryFragment' a collection of them?
public enum Segment: Hashable, Sendable {
case sql(String)
case binding(QueryBinding)
}

// TODO: Make 'private(set)' and add APIs to support extensibility like 'indent()'?
public internal(set) var segments: [Segment] = []

#if DEBUG
/// An array of parameterized statement bindings.
public var bindings: [QueryBinding]
#else
/// An array of parameterized statement bindings.
public package(set) var bindings: [QueryBinding]
#endif
fileprivate init(segments: [Segment]) {
self.segments = segments
}

init(_ string: String = "", _ bindings: [QueryBinding] = []) {
self.string = string
self.bindings = bindings
init(_ string: String = "") {
self.init(segments: [.sql(string)])
}

/// A Boolean value indicating whether the query fragment is empty.
public var isEmpty: Bool {
return string.isEmpty && bindings.isEmpty
segments.allSatisfy {
switch $0 {
case .sql(let sql):
sql.isEmpty
case .binding:
false
}
}
}

/// Appends the given fragment to this query fragment.
///
/// - Parameter other: Another query fragment.
public mutating func append(_ other: Self) {
string.append(other.string)
bindings.append(contentsOf: other.bindings)
segments.append(contentsOf: other.segments)
}

/// Appends a given query fragment to another fragment.
Expand All @@ -51,36 +53,18 @@ public struct QueryFragment: Hashable, Sendable, CustomDebugStringConvertible {
query += rhs
return query
}
}

extension QueryFragment: CustomDebugStringConvertible {
public var debugDescription: String {
var compiled = ""
var bindings = bindings
var currentDelimiter: Character?
compiled.reserveCapacity(string.count)
let delimiters: [Character: Character] = [
#"""#: #"""#,
"'": "'",
"`": "`",
"[": "]",
]
for character in string {
if let delimiter = currentDelimiter {
if delimiter == character,
compiled.last != character || compiled.last == delimiters[delimiter]
{
currentDelimiter = nil
}
compiled.append(character)
} else if delimiters.keys.contains(character) {
currentDelimiter = character
compiled.append(character)
} else if character == "?" {
compiled.append(bindings.removeFirst().debugDescription)
} else {
compiled.append(character)
segments.reduce(into: "") { debugDescription, segment in
switch segment {
case .sql(let sql):
debugDescription.append(sql)
case .binding(let binding):
debugDescription.append(binding.debugDescription)
}
}
return compiled
}
}

Expand All @@ -103,7 +87,7 @@ extension [QueryFragment] {

extension QueryFragment: ExpressibleByStringInterpolation {
public init(stringInterpolation: StringInterpolation) {
self.init(stringInterpolation.string, stringInterpolation.bindings)
self.init(segments: stringInterpolation.segments)
}

public init(stringLiteral value: String) {
Expand Down Expand Up @@ -132,16 +116,15 @@ extension QueryFragment: ExpressibleByStringInterpolation {
}

public struct StringInterpolation: StringInterpolationProtocol {
public var string = ""
public var bindings: [QueryBinding] = []
fileprivate var segments: [Segment] = []

public init(literalCapacity: Int, interpolationCount: Int) {
string.reserveCapacity(literalCapacity)
bindings.reserveCapacity(interpolationCount)
// TODO: Should all the segments' strings share the same contiguous storage as substring/span?
segments.reserveCapacity(interpolationCount)
}

public mutating func appendLiteral(_ literal: String) {
string.append(literal)
segments.append(.sql(literal))
}

/// Append a quoted fragment to the interpolation.
Expand All @@ -162,7 +145,7 @@ extension QueryFragment: ExpressibleByStringInterpolation {
quote sql: String,
delimiter: QuoteDelimiter = .identifier
) {
string.append(sql.quoted(delimiter))
segments.append(.sql(sql.quoted(delimiter)))
}

/// Append a raw SQL string to the interpolation.
Expand All @@ -174,7 +157,7 @@ extension QueryFragment: ExpressibleByStringInterpolation {
///
/// - Parameter sql: A raw query string.
public mutating func appendInterpolation(raw sql: String) {
string.append(sql)
segments.append(.sql(sql))
}

/// Append a raw lossless string to the interpolation.
Expand All @@ -191,23 +174,21 @@ extension QueryFragment: ExpressibleByStringInterpolation {
///
/// - Parameter sql: A raw query string.
public mutating func appendInterpolation(raw sql: some LosslessStringConvertible) {
string.append(sql.description)
segments.append(.sql(sql.description))
}

/// Append a query binding to the interpolation.
///
/// - Parameter binding: A query binding.
public mutating func appendInterpolation(_ binding: QueryBinding) {
string.append("?")
bindings.append(binding)
segments.append(.binding(binding))
}

/// Append a query fragment to the interpolation.
///
/// - Parameter fragment: A query fragment.
public mutating func appendInterpolation(_ fragment: QueryFragment) {
string.append(fragment.string)
bindings.append(contentsOf: fragment.bindings)
segments.append(contentsOf: fragment.segments)
}

/// Append a query expression to the interpolation.
Expand Down
12 changes: 10 additions & 2 deletions Sources/StructuredQueriesCore/TableAlias.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,16 @@ extension QueryFragment {
of _: T.Type, with _: A.Type
) -> QueryFragment {
var query = self
query.string = query.string
.replacingOccurrences(of: T.tableName.quoted(), with: A.aliasName.quoted())
for index in query.segments.indices {
switch query.segments[index] {
case .sql(let sql):
query.segments[index] = .sql(
sql.replacingOccurrences(of: T.tableName.quoted(), with: A.aliasName.quoted())
)
case .binding:
continue
}
}
return query
}
}
13 changes: 11 additions & 2 deletions Sources/StructuredQueriesSQLite/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,21 @@ public struct Database {
func withStatement<R>(
_ query: QueryFragment, body: (OpaquePointer) throws -> R
) throws -> R {
let (sql, bindings) = query.segments.reduce(into: (sql: "", bindings: [QueryBinding]())) {
switch $1 {
case .sql(let sql):
$0.sql.append(sql)
case .binding(let binding):
$0.sql.append("?")
$0.bindings.append(binding)
}
}
var statement: OpaquePointer?
let code = sqlite3_prepare_v2(storage.handle, query.string, -1, &statement, nil)
let code = sqlite3_prepare_v2(storage.handle, sql, -1, &statement, nil)
guard code == SQLITE_OK, let statement
else { throw SQLiteError(db: storage.handle) }
defer { sqlite3_finalize(statement) }
for (index, binding) in zip(Int32(1)..., query.bindings) {
for (index, binding) in zip(Int32(1)..., bindings) {
let result =
switch binding {
case .blob(let blob):
Expand Down