|
2 | 2 | // DatabaseQuery.swift |
3 | 3 | // feather-database |
4 | 4 | // |
5 | | -// Created by Tibor Bödecs on 2026. 01. 10.. |
| 5 | +// Created by Tibor Bödecs on 2026. 02. 07.. |
6 | 6 | // |
7 | 7 |
|
8 | | -/// A query description with SQL text and bindings. |
| 8 | +/// A database query with SQL text and bound parameters. |
9 | 9 | /// |
10 | | -/// Conforming types provide a query string and its bound parameters. |
11 | | -public protocol DatabaseQuery: Sendable { |
12 | | - /// The bindings type associated with this query. |
13 | | - /// |
14 | | - /// Use a `Sendable` type that represents bind parameters. |
15 | | - associatedtype Bindings: Sendable |
| 10 | +/// Use this type to construct database queries safely. |
| 11 | +public struct DatabaseQuery: Sendable, Equatable, Hashable, Codable { |
16 | 12 |
|
17 | 13 | /// The SQL text to execute. |
18 | 14 | /// |
19 | 15 | /// This is the raw SQL string for the query. |
20 | | - var sql: String { get } |
| 16 | + public var sql: String |
| 17 | + |
21 | 18 | /// The bound parameters for the SQL text. |
22 | 19 | /// |
23 | | - /// These bindings are passed alongside `sql`. |
24 | | - var bindings: Bindings { get } |
| 20 | + /// These values are passed alongside `sql`. |
| 21 | + public var bindings: [DatabaseQueryBindings] |
| 22 | + |
| 23 | + /// Create a query from raw SQL and bindings. |
| 24 | + /// |
| 25 | + /// Prefer string interpolation initializers when possible to bind values. |
| 26 | + /// - Parameters: |
| 27 | + /// - sql: The raw SQL string to execute. |
| 28 | + /// - bindings: The bound parameters for the SQL. |
| 29 | + public init( |
| 30 | + unsafeSQL sql: String, |
| 31 | + bindings: [DatabaseQueryBindings] = [] |
| 32 | + ) { |
| 33 | + self.sql = sql |
| 34 | + self.bindings = bindings |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +extension DatabaseQuery: ExpressibleByStringInterpolation { |
| 39 | + |
| 40 | + /// A string interpolation builder for database queries. |
| 41 | + /// |
| 42 | + /// Use interpolation to bind values safely into SQL text. |
| 43 | + public struct StringInterpolation: StringInterpolationProtocol, Sendable { |
| 44 | + |
| 45 | + /// The string literal type used by the interpolation. |
| 46 | + /// |
| 47 | + /// This matches the standard `String` literal type. |
| 48 | + public typealias StringLiteralType = String |
| 49 | + |
| 50 | + @usableFromInline |
| 51 | + var sql: String |
| 52 | + |
| 53 | + @usableFromInline |
| 54 | + var binds: [DatabaseQueryBindings] |
| 55 | + |
| 56 | + /// Create a new interpolation buffer. |
| 57 | + /// |
| 58 | + /// Use the provided capacities to preallocate storage. |
| 59 | + /// - Parameters: |
| 60 | + /// - literalCapacity: The expected literal character count. |
| 61 | + /// - interpolationCount: The expected number of interpolations. |
| 62 | + public init( |
| 63 | + literalCapacity: Int, |
| 64 | + interpolationCount: Int |
| 65 | + ) { |
| 66 | + self.sql = "" |
| 67 | + self.sql.reserveCapacity(literalCapacity) |
| 68 | + self.binds = [] |
| 69 | + self.binds.reserveCapacity(interpolationCount) |
| 70 | + } |
| 71 | + |
| 72 | + /// Append a literal string segment. |
| 73 | + /// |
| 74 | + /// This adds raw SQL text to the builder. |
| 75 | + /// - Parameter literal: The literal string segment. |
| 76 | + public mutating func appendLiteral( |
| 77 | + _ literal: String |
| 78 | + ) { |
| 79 | + sql.append(contentsOf: literal) |
| 80 | + } |
| 81 | + |
| 82 | + /// Append an interpolated optional string value. |
| 83 | + /// |
| 84 | + /// Non-nil values are bound, and nil values emit `NULL`. |
| 85 | + /// - Parameter value: The optional string value to interpolate. |
| 86 | + @inlinable |
| 87 | + public mutating func appendInterpolation( |
| 88 | + _ value: String? |
| 89 | + ) { |
| 90 | + switch value { |
| 91 | + case .some(let value): |
| 92 | + binds.append( |
| 93 | + .init( |
| 94 | + index: binds.count, |
| 95 | + binding: .string(value) |
| 96 | + ) |
| 97 | + ) |
| 98 | + sql.append(contentsOf: "{{\(binds.count)}}") |
| 99 | + case .none: |
| 100 | + sql.append(contentsOf: "NULL") |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + /// Append an interpolated integer value. |
| 105 | + /// |
| 106 | + /// The value is bound as an integer. |
| 107 | + /// - Parameter value: The integer value to interpolate. |
| 108 | + @inlinable |
| 109 | + public mutating func appendInterpolation( |
| 110 | + _ value: Int |
| 111 | + ) { |
| 112 | + binds.append( |
| 113 | + .init( |
| 114 | + index: binds.count, |
| 115 | + binding: .int(value) |
| 116 | + ) |
| 117 | + ) |
| 118 | + sql.append(contentsOf: "{{\(binds.count)}}") |
| 119 | + } |
| 120 | + |
| 121 | + /// Append an interpolated floating-point value. |
| 122 | + /// |
| 123 | + /// The value is bound as a floating-point value. |
| 124 | + /// - Parameter value: The double value to interpolate. |
| 125 | + @inlinable |
| 126 | + public mutating func appendInterpolation( |
| 127 | + _ value: Double |
| 128 | + ) { |
| 129 | + binds.append( |
| 130 | + .init( |
| 131 | + index: binds.count, |
| 132 | + binding: .double(value) |
| 133 | + ) |
| 134 | + ) |
| 135 | + sql.append(contentsOf: "{{\(binds.count)}}") |
| 136 | + } |
| 137 | + |
| 138 | + /// Append an interpolated string value. |
| 139 | + /// |
| 140 | + /// The value is bound as a text value. |
| 141 | + /// - Parameter value: The string value to interpolate. |
| 142 | + @inlinable |
| 143 | + public mutating func appendInterpolation( |
| 144 | + _ value: String |
| 145 | + ) { |
| 146 | + binds.append( |
| 147 | + .init( |
| 148 | + index: binds.count, |
| 149 | + binding: .string(value) |
| 150 | + ) |
| 151 | + ) |
| 152 | + sql.append(contentsOf: "{{\(binds.count)}}") |
| 153 | + } |
| 154 | + |
| 155 | + /// Append an unescaped SQL fragment. |
| 156 | + /// |
| 157 | + /// Use this only for trusted identifiers or SQL keywords. |
| 158 | + /// - Parameter interpolated: The raw SQL fragment to insert. |
| 159 | + @inlinable |
| 160 | + public mutating func appendInterpolation( |
| 161 | + unescaped interpolated: String |
| 162 | + ) { |
| 163 | + self.sql.append(contentsOf: interpolated) |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + /// Create a query from a string interpolation builder. |
| 168 | + /// |
| 169 | + /// This initializer is used by Swift string interpolation. |
| 170 | + /// - Parameter stringInterpolation: The interpolation builder. |
| 171 | + public init( |
| 172 | + stringInterpolation: StringInterpolation |
| 173 | + ) { |
| 174 | + self.sql = stringInterpolation.sql |
| 175 | + self.bindings = stringInterpolation.binds |
| 176 | + } |
| 177 | + |
| 178 | + /// Create a query from a string literal. |
| 179 | + /// |
| 180 | + /// This initializer does not add any bindings. |
| 181 | + /// - Parameter value: The literal SQL string. |
| 182 | + public init( |
| 183 | + stringLiteral value: String |
| 184 | + ) { |
| 185 | + self.sql = value |
| 186 | + self.bindings = [] |
| 187 | + } |
25 | 188 | } |
0 commit comments