Skip to content

Commit 8bd475b

Browse files
authored
Basic currency type for SQL query (#18)
- Add public query type - Add query tests - Updated README
1 parent 6c12c0c commit 8bd475b

9 files changed

Lines changed: 331 additions & 35 deletions

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
Abstract database component, providing a shared API surface for database drivers written in Swift.
44

5-
[![Release: 1.0.0-beta.3](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E3-F05138)](https://github.com/feather-framework/feather-database/releases/tag/1.0.0-beta.3)
5+
[
6+
![Release: 1.0.0-beta.4](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E4-F05138)
7+
](
8+
https://github.com/feather-framework/feather-database/releases/tag/1.0.0-beta.4
9+
)
610

711
## Features
812

@@ -31,7 +35,7 @@ Abstract database component, providing a shared API surface for database drivers
3135
Use Swift Package Manager; add the dependency to your `Package.swift` file:
3236

3337
```swift
34-
.package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.3"),
38+
.package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.4"),
3539
```
3640

3741
Then add `FeatherDatabase` to your target dependencies:

Sources/FeatherDatabase/DatabaseConnection.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public protocol DatabaseConnection: Sendable {
1515
/// The query type supported by this connection.
1616
///
1717
/// Use this to define the SQL and bindings type.
18-
associatedtype Query: DatabaseQuery
18+
// associatedtype Query: DatabaseQuery
1919

2020
/// The row sequence type produced by this connection.
2121
///
@@ -36,7 +36,7 @@ public protocol DatabaseConnection: Sendable {
3636
/// - Returns: The result of the query execution.
3737
@discardableResult
3838
func run<T: Sendable>(
39-
query: Query,
39+
query: DatabaseQuery,
4040
_ handler: (RowSequence) async throws -> T
4141
) async throws(DatabaseError) -> T
4242
}

Sources/FeatherDatabase/DatabaseQuery.swift

Lines changed: 174 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,187 @@
22
// DatabaseQuery.swift
33
// feather-database
44
//
5-
// Created by Tibor Bödecs on 2026. 01. 10..
5+
// Created by Tibor Bödecs on 2026. 02. 07..
66
//
77

8-
/// A query description with SQL text and bindings.
8+
/// A database query with SQL text and bound parameters.
99
///
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 {
1612

1713
/// The SQL text to execute.
1814
///
1915
/// This is the raw SQL string for the query.
20-
var sql: String { get }
16+
public var sql: String
17+
2118
/// The bound parameters for the SQL text.
2219
///
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+
}
25188
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// DatabaseQueryBinding.swift
3+
// feather-database
4+
//
5+
// Created by Tibor Bödecs on 2026. 02. 07..
6+
//
7+
8+
/// A single bindable value used by a database query.
9+
public enum DatabaseQueryBinding: Sendable, Equatable, Hashable, Codable {
10+
/// A text value.
11+
case string(String)
12+
/// An integer value.
13+
case int(Int)
14+
/// A floating-point value.
15+
case double(Double)
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// DatabaseQueryBindings.swift
3+
// feather-database
4+
//
5+
// Created by Tibor Bödecs on 2026. 02. 07..
6+
//
7+
8+
/// A bound value with a stable index for a query.
9+
public struct DatabaseQueryBindings: Sendable, Equatable, Hashable, Codable {
10+
11+
/// The zero-based binding index.
12+
public var index: Int
13+
/// The bound value.
14+
public var binding: DatabaseQueryBinding
15+
16+
/// Create a new indexed binding.
17+
/// - Parameters:
18+
/// - index: The zero-based binding index.
19+
/// - binding: The bound value.
20+
public init(
21+
index: Int,
22+
binding: DatabaseQueryBinding
23+
) {
24+
self.index = index
25+
self.binding = binding
26+
}
27+
}

0 commit comments

Comments
 (0)