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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Migration guides

Learn how to upgrade your application to the latest version of StructuredQueries.

## Overview

StructuredQueries is under constant development, and we are always looking for ways to simplify the
library and make it more powerful. As such, we often need to deprecate certain APIs in favor of
newer ones. We recommend people update their code as quickly as possible to the newest APIs, and
these guides contain tips to do so.

> Important: Before following any particular migration guide be sure you have followed all the
> preceding migration guides.

## Topics

- <doc:MigratingTo0.16>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Migrating to 0.16

StructuredQueries 0.16 introduces powerful tools for user-defined SQLite functions, with some
breaking changes for those defining custom query representations.

## Overview

StructuredQueries recently introduced a new module, StructuredQueriesSQLite, and with it a new macro
for defining Swift functions that can be called from a query. It's called `@DatabaseFunction`, and
can annotate any function that works with query-representable types.

For example, an `exclaim` function can be defined like so:

```swift
@DatabaseFunction
func exclaim(_ string: String) -> String {
string.localizedUppercase + "!"
}
```

And will be immediately callable in a query by prefixing the function with `$`:

```swift
Reminder.select { $exclaim($0.title) }
// SELECT "exclaim"("reminders"."title") FROM "reminders"
```

For the query to successfully execute, you must also add the function to your SQLite database
connection. This can be done in [SharingGRDB] (0.7.0+) using the `Database.add(function:)` method,
_e.g._ when you first configure things:

[SharingGRDB]: https://github.com/pointfreeco/sharing-grdb

```swift
var configuration = Configuration()
configuration.prepareDatabase { db in
db.add(function: $exclaim)
}
```

> Tip: Use the `isDeterministic` parameter for functions that always return the same value from the
> same arguments. SQLite's query planner can optimize these functions.
>
> ```swift
> @DatabaseFunction(isDeterministic: true)
> func exclaim(_ string: String) -> String {
> string.localizedUppercase + "!"
> }
> ```

### Custom representations

To define a type that works with a custom representation, like JSON, you can use the `as` parameter
of the macro:

```swift
@DatabaseFunction(
as: (([String].JSONRepresentation) -> [String].JSONRepresentation).self
)
func jsonArrayExclaim(_ strings: [String]) -> [String] {
strings.map { $0.localizedUppercase + "!" }
}
```

### Breaking change: user-defined representations

To power things, a new initializer, ``QueryBindable/init(queryBinding:)``, was added to the
``QueryBindable`` protocol. While most code should continue to compile, if you define your own
query representations that conform to ``QueryRepresentable``, you will need to define this
initializer upon upgrading.

For example, `JSONRepresentation` added the following initializer:

```swift
public init?(queryBinding: QueryBinding) {
guard case .text(let json) = queryBinding else { return nil }
guard let queryOutput = try? jsonDecoder.decode(
QueryOutput.self, from: Data(json.utf8)
)
else { return nil }
self.init(queryOutput: queryOutput)
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,7 @@ reading to learn more about building SQL with StructuredQueries.

- <doc:FullTextSearch>
- <doc:Integration>

### Migration guides

- <doc:MigrationGuides>
16 changes: 16 additions & 0 deletions Sources/StructuredQueriesCore/Internal/Deprecations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ extension Date.ISO8601Representation: QueryBindable {
public var queryBinding: QueryBinding {
.text(queryOutput.iso8601String)
}

public init?(queryBinding: QueryBinding) {
guard
case .text(let iso8601String) = queryBinding,
let queryOutput = try? Date(iso8601String: iso8601String)
else { return nil }
self.init(queryOutput: queryOutput)
}
}

@available(
Expand Down Expand Up @@ -226,6 +234,14 @@ extension UUID.LowercasedRepresentation: QueryBindable {
public var queryBinding: QueryBinding {
.text(queryOutput.uuidString.lowercased())
}

public init?(queryBinding: QueryBinding) {
guard
case .text(let uuidString) = queryBinding,
let uuid = UUID(uuidString: uuidString)
else { return nil }
self.init(queryOutput: uuid)
}
}

@available(
Expand Down
10 changes: 10 additions & 0 deletions Sources/StructuredQueriesCore/QueryBindable+Foundation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ extension Data: QueryBindable {
.blob([UInt8](self))
}

public init?(queryBinding: QueryBinding) {
guard case .blob(let bytes) = queryBinding else { return nil }
self.init(bytes)
}

public init(decoder: inout some QueryDecoder) throws {
try self.init([UInt8](decoder: &decoder))
}
Expand All @@ -15,6 +20,11 @@ extension URL: QueryBindable {
.text(absoluteString)
}

public init?(queryBinding: QueryBinding) {
guard case .text(let string) = queryBinding else { return nil }
self.init(string: string)
}

public init(decoder: inout some QueryDecoder) throws {
guard let url = Self(string: try String(decoder: &decoder)) else {
throw InvalidURL()
Expand Down
49 changes: 44 additions & 5 deletions Sources/StructuredQueriesCore/QueryBindable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ public protocol QueryBindable: QueryRepresentable, QueryExpression where QueryVa

extension QueryBindable {
public var queryFragment: QueryFragment { "\(queryBinding)" }

public init?(queryBinding: QueryBinding) {
guard let queryValue = QueryValue(queryBinding: queryBinding) else { return nil }
self.init(queryBinding: queryValue.queryBinding)
}
}

extension [UInt8]: QueryBindable, QueryExpression {
Expand Down Expand Up @@ -53,22 +48,42 @@ extension Date: QueryBindable {

extension Float: QueryBindable {
public var queryBinding: QueryBinding { .double(Double(self)) }
public init?(queryBinding: QueryBinding) {
guard case .double(let value) = queryBinding else { return nil }
self.init(value)
}
}

extension Int: QueryBindable {
public var queryBinding: QueryBinding { .int(Int64(self)) }
public init?(queryBinding: QueryBinding) {
guard case .int(let value) = queryBinding else { return nil }
self.init(value)
}
}

extension Int8: QueryBindable {
public var queryBinding: QueryBinding { .int(Int64(self)) }
public init?(queryBinding: QueryBinding) {
guard case .int(let value) = queryBinding else { return nil }
self.init(value)
}
}

extension Int16: QueryBindable {
public var queryBinding: QueryBinding { .int(Int64(self)) }
public init?(queryBinding: QueryBinding) {
guard case .int(let value) = queryBinding else { return nil }
self.init(value)
}
}

extension Int32: QueryBindable {
public var queryBinding: QueryBinding { .int(Int64(self)) }
public init?(queryBinding: QueryBinding) {
guard case .int(let value) = queryBinding else { return nil }
self.init(value)
}
}

extension Int64: QueryBindable {
Expand All @@ -89,14 +104,26 @@ extension String: QueryBindable {

extension UInt8: QueryBindable {
public var queryBinding: QueryBinding { .int(Int64(self)) }
public init?(queryBinding: QueryBinding) {
guard case .int(let value) = queryBinding else { return nil }
self.init(value)
}
}

extension UInt16: QueryBindable {
public var queryBinding: QueryBinding { .int(Int64(self)) }
public init?(queryBinding: QueryBinding) {
guard case .int(let value) = queryBinding else { return nil }
self.init(value)
}
}

extension UInt32: QueryBindable {
public var queryBinding: QueryBinding { .int(Int64(self)) }
public init?(queryBinding: QueryBinding) {
guard case .int(let value) = queryBinding else { return nil }
self.init(value)
}
}

extension UInt64: QueryBindable {
Expand All @@ -107,6 +134,10 @@ extension UInt64: QueryBindable {
return .int(Int64(self))
}
}
public init?(queryBinding: QueryBinding) {
guard case .int(let value) = queryBinding, value >= UInt64.min else { return nil }
self.init(value)
}
}

extension UUID: QueryBindable {
Expand Down Expand Up @@ -146,8 +177,16 @@ extension DefaultStringInterpolation {

extension QueryBindable where Self: LosslessStringConvertible {
public var queryBinding: QueryBinding { description.queryBinding }
public init?(queryBinding: QueryBinding) {
guard let description = String(queryBinding: queryBinding) else { return nil }
self.init(description)
}
}

extension QueryBindable where Self: RawRepresentable, RawValue: QueryBindable {
public var queryBinding: QueryBinding { rawValue.queryBinding }
public init?(queryBinding: QueryBinding) {
guard let rawValue = RawValue(queryBinding: queryBinding) else { return nil }
self.init(rawValue: rawValue)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@ public struct _CodableJSONRepresentation<QueryOutput: Codable>: QueryRepresentab
public init(queryOutput: QueryOutput) {
self.queryOutput = queryOutput
}

public init(decoder: inout some QueryDecoder) throws {
self.init(
queryOutput: try jsonDecoder.decode(
QueryOutput.self,
from: Data(String(decoder: &decoder).utf8)
)
)
}
}

extension Decodable where Self: Encodable {
Expand Down Expand Up @@ -46,6 +37,24 @@ extension _CodableJSONRepresentation: QueryBindable {
return .invalid(error)
}
}

public init?(queryBinding: QueryBinding) {
guard case .text(let value) = queryBinding else { return nil }
guard let queryOutput = try? jsonDecoder.decode(QueryOutput.self, from: Data(value.utf8))
else { return nil }
self.init(queryOutput: queryOutput)
}
}

extension _CodableJSONRepresentation: QueryDecodable {
public init(decoder: inout some QueryDecoder) throws {
self.init(
queryOutput: try jsonDecoder.decode(
QueryOutput.self,
from: Data(String(decoder: &decoder).utf8)
)
)
}
}

extension _CodableJSONRepresentation: SQLiteType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ extension Date.JulianDayRepresentation: QueryBindable {
public var queryBinding: QueryBinding {
.double(2440587.5 + queryOutput.timeIntervalSince1970 / 86400)
}

public init?(queryBinding: QueryBinding) {
guard case .double(let value) = queryBinding else { return nil }
self.init(queryOutput: Date(timeIntervalSince1970: (value - 2440587.5) * 86400))
}
}

extension Date.JulianDayRepresentation: QueryDecodable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ extension Date.UnixTimeRepresentation: QueryBindable {
public var queryBinding: QueryBinding {
.int(Int64(queryOutput.timeIntervalSince1970))
}

public init?(queryBinding: QueryBinding) {
guard case .int(let timeIntervalSince1970) = queryBinding else { return nil }
self.init(queryOutput: Date(timeIntervalSince1970: Double(timeIntervalSince1970)))
}
}

extension Date.UnixTimeRepresentation: QueryDecodable {
Expand Down
10 changes: 10 additions & 0 deletions Sources/StructuredQueriesCore/QueryRepresentable/UUID+Bytes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ extension UUID.BytesRepresentation: QueryBindable {
public var queryBinding: QueryBinding {
.blob(withUnsafeBytes(of: queryOutput.uuid, [UInt8].init))
}

public init?(queryBinding: QueryBinding) {
guard case .blob(let bytes) = queryBinding else { return nil }
guard bytes.count == 16 else { return nil }
self.init(
queryOutput: bytes.withUnsafeBytes {
UUID(uuid: $0.load(as: uuid_t.self))
}
)
}
}

extension UUID.BytesRepresentation: QueryDecodable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ extension UUID? {
}

extension UUID.UppercasedRepresentation: QueryBindable {
public init?(queryBinding: QueryBinding) {
guard
case .text(let uuidString) = queryBinding,
let uuid = UUID(uuidString: uuidString)
else { return nil }
self.init(queryOutput: uuid)
}

public var queryBinding: QueryBinding {
.text(queryOutput.uuidString)
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/StructuredQueriesCore/TableAlias.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ extension TableAlias: QueryBindable where Base: QueryBindable {
public var queryBinding: QueryBinding {
base.queryBinding
}

public init?(queryBinding: QueryBinding) {
guard let base = Base(queryBinding: queryBinding) else { return nil }
self.init(base: base)
}
}

extension TableAlias: QueryDecodable where Base: QueryDecodable {
Expand Down
Loading