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
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
121 changes: 61 additions & 60 deletions Sources/StructuredQueriesCore/QueryFragment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,44 @@ 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 {
/// A segment of a query fragment.
public enum Segment: Hashable, Sendable {
/// A raw SQL fragment.
case sql(String)

#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
/// A binding.
case binding(QueryBinding)
}

/// An array of segments backing this query fragment.
public internal(set) var segments: [Segment] = []

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 @@ -52,35 +57,35 @@ public struct QueryFragment: Hashable, Sendable, CustomDebugStringConvertible {
return query
}

/// Returns a prepared SQL string and associated bindings for this query.
///
/// - Parameter template: Prepare a template string for a binding at a given 1-based offset.
/// - Returns: A SQL string and array of associated bindings.
public func prepare(
_ template: (_ offset: Int) -> String
) -> (sql: String, bindings: [QueryBinding]) {
segments.enumerated().reduce(into: (sql: "", bindings: [QueryBinding]())) {
switch $1.element {
case .sql(let sql):
$0.sql.append(sql)
case .binding(let binding):
$0.sql.append(template($1.offset + 1))
$0.bindings.append(binding)
}
}
}
}

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 +108,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 +137,14 @@ 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)
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 +165,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 +177,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 +194,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
}
}
5 changes: 3 additions & 2 deletions Sources/StructuredQueriesSQLite/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,13 @@ public struct Database {
func withStatement<R>(
_ query: QueryFragment, body: (OpaquePointer) throws -> R
) throws -> R {
let (sql, bindings) = query.prepare { _ in "?" }
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