Skip to content

Commit 14f79c6

Browse files
authored
Database Functions Improvements (#154)
* Fix database function binding overflows We need to explicitly define more initializers. This does make me think `QueryBindable` needs to track the intermediate representation better so we could generate these. * wip * wip * wip * wip * wip * wip * wip * Update MigratingTo0.16.md * wip
1 parent e53cd5c commit 14f79c6

File tree

18 files changed

+654
-80
lines changed

18 files changed

+654
-80
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Migration guides
2+
3+
Learn how to upgrade your application to the latest version of StructuredQueries.
4+
5+
## Overview
6+
7+
StructuredQueries is under constant development, and we are always looking for ways to simplify the
8+
library and make it more powerful. As such, we often need to deprecate certain APIs in favor of
9+
newer ones. We recommend people update their code as quickly as possible to the newest APIs, and
10+
these guides contain tips to do so.
11+
12+
> Important: Before following any particular migration guide be sure you have followed all the
13+
> preceding migration guides.
14+
15+
## Topics
16+
17+
- <doc:MigratingTo0.16>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Migrating to 0.16
2+
3+
StructuredQueries 0.16 introduces powerful tools for user-defined SQLite functions, with some
4+
breaking changes for those defining custom query representations.
5+
6+
## Overview
7+
8+
StructuredQueries recently introduced a new module, StructuredQueriesSQLite, and with it a new macro
9+
for defining Swift functions that can be called from a query. It's called `@DatabaseFunction`, and
10+
can annotate any function that works with query-representable types.
11+
12+
For example, an `exclaim` function can be defined like so:
13+
14+
```swift
15+
@DatabaseFunction
16+
func exclaim(_ string: String) -> String {
17+
string.localizedUppercase + "!"
18+
}
19+
```
20+
21+
And will be immediately callable in a query by prefixing the function with `$`:
22+
23+
```swift
24+
Reminder.select { $exclaim($0.title) }
25+
// SELECT "exclaim"("reminders"."title") FROM "reminders"
26+
```
27+
28+
For the query to successfully execute, you must also add the function to your SQLite database
29+
connection. This can be done in [SharingGRDB] (0.7.0+) using the `Database.add(function:)` method,
30+
_e.g._ when you first configure things:
31+
32+
[SharingGRDB]: https://github.com/pointfreeco/sharing-grdb
33+
34+
```swift
35+
var configuration = Configuration()
36+
configuration.prepareDatabase { db in
37+
db.add(function: $exclaim)
38+
}
39+
```
40+
41+
> Tip: Use the `isDeterministic` parameter for functions that always return the same value from the
42+
> same arguments. SQLite's query planner can optimize these functions.
43+
>
44+
> ```swift
45+
> @DatabaseFunction(isDeterministic: true)
46+
> func exclaim(_ string: String) -> String {
47+
> string.localizedUppercase + "!"
48+
> }
49+
> ```
50+
51+
### Custom representations
52+
53+
To define a type that works with a custom representation, like JSON, you can use the `as` parameter
54+
of the macro:
55+
56+
```swift
57+
@DatabaseFunction(
58+
as: (([String].JSONRepresentation) -> [String].JSONRepresentation).self
59+
)
60+
func jsonArrayExclaim(_ strings: [String]) -> [String] {
61+
strings.map { $0.localizedUppercase + "!" }
62+
}
63+
```
64+
65+
### Breaking change: user-defined representations
66+
67+
To power things, a new initializer, ``QueryBindable/init(queryBinding:)``, was added to the
68+
``QueryBindable`` protocol. While most code should continue to compile, if you define your own
69+
query representations that conform to ``QueryRepresentable``, you will need to define this
70+
initializer upon upgrading.
71+
72+
For example, `JSONRepresentation` added the following initializer:
73+
74+
```swift
75+
public init?(queryBinding: QueryBinding) {
76+
guard case .text(let json) = queryBinding else { return nil }
77+
guard let queryOutput = try? jsonDecoder.decode(
78+
QueryOutput.self, from: Data(json.utf8)
79+
)
80+
else { return nil }
81+
self.init(queryOutput: queryOutput)
82+
}
83+
```

Sources/StructuredQueriesCore/Documentation.docc/StructuredQueriesCore.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,7 @@ reading to learn more about building SQL with StructuredQueries.
140140

141141
- <doc:FullTextSearch>
142142
- <doc:Integration>
143+
144+
### Migration guides
145+
146+
- <doc:MigrationGuides>

Sources/StructuredQueriesCore/Internal/Deprecations.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ extension Date.ISO8601Representation: QueryBindable {
169169
public var queryBinding: QueryBinding {
170170
.text(queryOutput.iso8601String)
171171
}
172+
173+
public init?(queryBinding: QueryBinding) {
174+
guard
175+
case .text(let iso8601String) = queryBinding,
176+
let queryOutput = try? Date(iso8601String: iso8601String)
177+
else { return nil }
178+
self.init(queryOutput: queryOutput)
179+
}
172180
}
173181

174182
@available(
@@ -226,6 +234,14 @@ extension UUID.LowercasedRepresentation: QueryBindable {
226234
public var queryBinding: QueryBinding {
227235
.text(queryOutput.uuidString.lowercased())
228236
}
237+
238+
public init?(queryBinding: QueryBinding) {
239+
guard
240+
case .text(let uuidString) = queryBinding,
241+
let uuid = UUID(uuidString: uuidString)
242+
else { return nil }
243+
self.init(queryOutput: uuid)
244+
}
229245
}
230246

231247
@available(

Sources/StructuredQueriesCore/QueryBindable+Foundation.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ extension Data: QueryBindable {
55
.blob([UInt8](self))
66
}
77

8+
public init?(queryBinding: QueryBinding) {
9+
guard case .blob(let bytes) = queryBinding else { return nil }
10+
self.init(bytes)
11+
}
12+
813
public init(decoder: inout some QueryDecoder) throws {
914
try self.init([UInt8](decoder: &decoder))
1015
}
@@ -15,6 +20,11 @@ extension URL: QueryBindable {
1520
.text(absoluteString)
1621
}
1722

23+
public init?(queryBinding: QueryBinding) {
24+
guard case .text(let string) = queryBinding else { return nil }
25+
self.init(string: string)
26+
}
27+
1828
public init(decoder: inout some QueryDecoder) throws {
1929
guard let url = Self(string: try String(decoder: &decoder)) else {
2030
throw InvalidURL()

Sources/StructuredQueriesCore/QueryBindable.swift

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@ public protocol QueryBindable: QueryRepresentable, QueryExpression where QueryVa
1616

1717
extension QueryBindable {
1818
public var queryFragment: QueryFragment { "\(queryBinding)" }
19-
20-
public init?(queryBinding: QueryBinding) {
21-
guard let queryValue = QueryValue(queryBinding: queryBinding) else { return nil }
22-
self.init(queryBinding: queryValue.queryBinding)
23-
}
2419
}
2520

2621
extension [UInt8]: QueryBindable, QueryExpression {
@@ -53,22 +48,42 @@ extension Date: QueryBindable {
5348

5449
extension Float: QueryBindable {
5550
public var queryBinding: QueryBinding { .double(Double(self)) }
51+
public init?(queryBinding: QueryBinding) {
52+
guard case .double(let value) = queryBinding else { return nil }
53+
self.init(value)
54+
}
5655
}
5756

5857
extension Int: QueryBindable {
5958
public var queryBinding: QueryBinding { .int(Int64(self)) }
59+
public init?(queryBinding: QueryBinding) {
60+
guard case .int(let value) = queryBinding else { return nil }
61+
self.init(value)
62+
}
6063
}
6164

6265
extension Int8: QueryBindable {
6366
public var queryBinding: QueryBinding { .int(Int64(self)) }
67+
public init?(queryBinding: QueryBinding) {
68+
guard case .int(let value) = queryBinding else { return nil }
69+
self.init(value)
70+
}
6471
}
6572

6673
extension Int16: QueryBindable {
6774
public var queryBinding: QueryBinding { .int(Int64(self)) }
75+
public init?(queryBinding: QueryBinding) {
76+
guard case .int(let value) = queryBinding else { return nil }
77+
self.init(value)
78+
}
6879
}
6980

7081
extension Int32: QueryBindable {
7182
public var queryBinding: QueryBinding { .int(Int64(self)) }
83+
public init?(queryBinding: QueryBinding) {
84+
guard case .int(let value) = queryBinding else { return nil }
85+
self.init(value)
86+
}
7287
}
7388

7489
extension Int64: QueryBindable {
@@ -89,14 +104,26 @@ extension String: QueryBindable {
89104

90105
extension UInt8: QueryBindable {
91106
public var queryBinding: QueryBinding { .int(Int64(self)) }
107+
public init?(queryBinding: QueryBinding) {
108+
guard case .int(let value) = queryBinding else { return nil }
109+
self.init(value)
110+
}
92111
}
93112

94113
extension UInt16: QueryBindable {
95114
public var queryBinding: QueryBinding { .int(Int64(self)) }
115+
public init?(queryBinding: QueryBinding) {
116+
guard case .int(let value) = queryBinding else { return nil }
117+
self.init(value)
118+
}
96119
}
97120

98121
extension UInt32: QueryBindable {
99122
public var queryBinding: QueryBinding { .int(Int64(self)) }
123+
public init?(queryBinding: QueryBinding) {
124+
guard case .int(let value) = queryBinding else { return nil }
125+
self.init(value)
126+
}
100127
}
101128

102129
extension UInt64: QueryBindable {
@@ -107,6 +134,10 @@ extension UInt64: QueryBindable {
107134
return .int(Int64(self))
108135
}
109136
}
137+
public init?(queryBinding: QueryBinding) {
138+
guard case .int(let value) = queryBinding, value >= UInt64.min else { return nil }
139+
self.init(value)
140+
}
110141
}
111142

112143
extension UUID: QueryBindable {
@@ -146,8 +177,16 @@ extension DefaultStringInterpolation {
146177

147178
extension QueryBindable where Self: LosslessStringConvertible {
148179
public var queryBinding: QueryBinding { description.queryBinding }
180+
public init?(queryBinding: QueryBinding) {
181+
guard let description = String(queryBinding: queryBinding) else { return nil }
182+
self.init(description)
183+
}
149184
}
150185

151186
extension QueryBindable where Self: RawRepresentable, RawValue: QueryBindable {
152187
public var queryBinding: QueryBinding { rawValue.queryBinding }
188+
public init?(queryBinding: QueryBinding) {
189+
guard let rawValue = RawValue(queryBinding: queryBinding) else { return nil }
190+
self.init(rawValue: rawValue)
191+
}
153192
}

Sources/StructuredQueriesCore/QueryRepresentable/Codable+JSON.swift

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,6 @@ public struct _CodableJSONRepresentation<QueryOutput: Codable>: QueryRepresentab
66
public init(queryOutput: QueryOutput) {
77
self.queryOutput = queryOutput
88
}
9-
10-
public init(decoder: inout some QueryDecoder) throws {
11-
self.init(
12-
queryOutput: try jsonDecoder.decode(
13-
QueryOutput.self,
14-
from: Data(String(decoder: &decoder).utf8)
15-
)
16-
)
17-
}
189
}
1910

2011
extension Decodable where Self: Encodable {
@@ -46,6 +37,24 @@ extension _CodableJSONRepresentation: QueryBindable {
4637
return .invalid(error)
4738
}
4839
}
40+
41+
public init?(queryBinding: QueryBinding) {
42+
guard case .text(let value) = queryBinding else { return nil }
43+
guard let queryOutput = try? jsonDecoder.decode(QueryOutput.self, from: Data(value.utf8))
44+
else { return nil }
45+
self.init(queryOutput: queryOutput)
46+
}
47+
}
48+
49+
extension _CodableJSONRepresentation: QueryDecodable {
50+
public init(decoder: inout some QueryDecoder) throws {
51+
self.init(
52+
queryOutput: try jsonDecoder.decode(
53+
QueryOutput.self,
54+
from: Data(String(decoder: &decoder).utf8)
55+
)
56+
)
57+
}
4958
}
5059

5160
extension _CodableJSONRepresentation: SQLiteType {

Sources/StructuredQueriesCore/QueryRepresentable/Date+JulianDay.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ extension Date.JulianDayRepresentation: QueryBindable {
3030
public var queryBinding: QueryBinding {
3131
.double(2440587.5 + queryOutput.timeIntervalSince1970 / 86400)
3232
}
33+
34+
public init?(queryBinding: QueryBinding) {
35+
guard case .double(let value) = queryBinding else { return nil }
36+
self.init(queryOutput: Date(timeIntervalSince1970: (value - 2440587.5) * 86400))
37+
}
3338
}
3439

3540
extension Date.JulianDayRepresentation: QueryDecodable {

Sources/StructuredQueriesCore/QueryRepresentable/Date+UnixTime.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ extension Date.UnixTimeRepresentation: QueryBindable {
3030
public var queryBinding: QueryBinding {
3131
.int(Int64(queryOutput.timeIntervalSince1970))
3232
}
33+
34+
public init?(queryBinding: QueryBinding) {
35+
guard case .int(let timeIntervalSince1970) = queryBinding else { return nil }
36+
self.init(queryOutput: Date(timeIntervalSince1970: Double(timeIntervalSince1970)))
37+
}
3338
}
3439

3540
extension Date.UnixTimeRepresentation: QueryDecodable {

Sources/StructuredQueriesCore/QueryRepresentable/UUID+Bytes.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ extension UUID.BytesRepresentation: QueryBindable {
3030
public var queryBinding: QueryBinding {
3131
.blob(withUnsafeBytes(of: queryOutput.uuid, [UInt8].init))
3232
}
33+
34+
public init?(queryBinding: QueryBinding) {
35+
guard case .blob(let bytes) = queryBinding else { return nil }
36+
guard bytes.count == 16 else { return nil }
37+
self.init(
38+
queryOutput: bytes.withUnsafeBytes {
39+
UUID(uuid: $0.load(as: uuid_t.self))
40+
}
41+
)
42+
}
3343
}
3444

3545
extension UUID.BytesRepresentation: QueryDecodable {

0 commit comments

Comments
 (0)