Skip to content

Commit d7f73e2

Browse files
committed
Support for StructuredQueries' @DatabaseFunction macro
See pointfreeco/swift-structured-queries#151 for more details.
1 parent 1d34f53 commit d7f73e2

File tree

5 files changed

+130
-4
lines changed

5 files changed

+130
-4
lines changed

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ let package = Package(
3939
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"),
4040
.package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"),
4141
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"),
42-
.package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.13.0"),
42+
.package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "custom-functions"),
4343
],
4444
targets: [
4545
.target(
@@ -82,13 +82,15 @@ let package = Package(
8282
.product(name: "Dependencies", package: "swift-dependencies"),
8383
.product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
8484
.product(name: "StructuredQueriesCore", package: "swift-structured-queries"),
85+
.product(name: "StructuredQueriesSQLiteCore", package: "swift-structured-queries"),
8586
]
8687
),
8788
.target(
8889
name: "StructuredQueriesGRDB",
8990
dependencies: [
9091
"StructuredQueriesGRDBCore",
9192
.product(name: "StructuredQueries", package: "swift-structured-queries"),
93+
.product(name: "StructuredQueriesSQLite", package: "swift-structured-queries"),
9294
]
9395
),
9496
.testTarget(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
@_exported import StructuredQueries
2+
@_exported import StructuredQueriesSQLite
23
@_exported import StructuredQueriesGRDBCore
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import GRDB
2+
import GRDBSQLite
3+
import Foundation
4+
5+
extension ScalarDatabaseFunction {
6+
/// Adds a user-defined `@DatabaseFunction` to a connection.
7+
///
8+
/// - Parameter db: A database connection.
9+
public func install(db: Database) {
10+
let db = db.sqliteConnection
11+
let box = Unmanaged.passRetained(ScalarDatabaseFunctionBox(self)).toOpaque()
12+
sqlite3_create_function_v2(
13+
db,
14+
name,
15+
argumentCount,
16+
textEncoding,
17+
box,
18+
{ context, argumentCount, arguments in
19+
Unmanaged<ScalarDatabaseFunctionBox>
20+
.fromOpaque(sqlite3_user_data(context))
21+
.takeUnretainedValue()
22+
.function
23+
.invoke([QueryBinding](argumentCount: argumentCount, arguments: arguments))
24+
.result(db: context)
25+
},
26+
nil,
27+
nil,
28+
{ context in
29+
guard let context else { return }
30+
Unmanaged<ScalarDatabaseFunctionBox>.fromOpaque(context).release()
31+
}
32+
)
33+
}
34+
35+
/// Deletes a user-defined `@DatabaseFunction` from a connection.
36+
///
37+
/// - Parameter db: A database connection.
38+
public func uninstall(db: Database) {
39+
let db = db.sqliteConnection
40+
sqlite3_create_function_v2(
41+
db,
42+
name,
43+
argumentCount,
44+
textEncoding,
45+
nil,
46+
nil,
47+
nil,
48+
nil,
49+
nil
50+
)
51+
}
52+
53+
private var argumentCount: Int32 {
54+
Int32(argumentCount ?? -1)
55+
}
56+
57+
private var textEncoding: Int32 {
58+
SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0)
59+
}
60+
}
61+
62+
private final class ScalarDatabaseFunctionBox {
63+
let function: any ScalarDatabaseFunction
64+
init(_ function: some ScalarDatabaseFunction) {
65+
self.function = function
66+
}
67+
}
68+
69+
extension [QueryBinding] {
70+
fileprivate init(argumentCount: Int32, arguments: UnsafeMutablePointer<OpaquePointer?>?) {
71+
self = (0..<argumentCount).map { offset in
72+
let value = arguments?[Int(offset)]
73+
switch sqlite3_value_type(value) {
74+
case SQLITE_BLOB:
75+
if let blob = sqlite3_value_blob(value) {
76+
let count = Int(sqlite3_value_bytes(value))
77+
let buffer = UnsafeRawBufferPointer(start: blob, count: count)
78+
return .blob([UInt8](buffer))
79+
} else {
80+
return .blob([])
81+
}
82+
case SQLITE_FLOAT:
83+
return .double(sqlite3_value_double(value))
84+
case SQLITE_INTEGER:
85+
return .int(sqlite3_value_int64(value))
86+
case SQLITE_NULL:
87+
return .null
88+
case SQLITE_TEXT:
89+
return .text(String(cString: UnsafePointer(sqlite3_value_text(value))))
90+
default:
91+
return .invalid(UnknownType())
92+
}
93+
}
94+
}
95+
96+
private struct UnknownType: Error {}
97+
}
98+
99+
extension QueryBinding {
100+
fileprivate func result(db: OpaquePointer?) {
101+
switch self {
102+
case .blob(let value):
103+
sqlite3_result_blob(db, Array(value), Int32(value.count), SQLITE_TRANSIENT)
104+
case .double(let value):
105+
sqlite3_result_double(db, value)
106+
case .date(let value):
107+
sqlite3_result_text(db, value.iso8601String, -1, SQLITE_TRANSIENT)
108+
case .int(let value):
109+
sqlite3_result_int64(db, value)
110+
case .null:
111+
sqlite3_result_null(db)
112+
case .text(let value):
113+
sqlite3_result_text(db, value, -1, SQLITE_TRANSIENT)
114+
case .uuid(let value):
115+
sqlite3_result_text(db, value.uuidString.lowercased(), -1, SQLITE_TRANSIENT)
116+
case .invalid(let error):
117+
sqlite3_result_error(db, error.underlyingError.localizedDescription, -1)
118+
}
119+
}
120+
}
121+
122+
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
@_exported import StructuredQueriesCore
2+
@_exported import StructuredQueriesSQLiteCore

0 commit comments

Comments
 (0)