Skip to content

Commit 09e4c8a

Browse files
authored
Support for StructuredQueries' @DatabaseFunction macro (#145)
* Support for StructuredQueries' `@DatabaseFunction` macro See pointfreeco/swift-structured-queries#151 for more details. * wip * wip * test * wip * wip
1 parent 2ac9e6c commit 09e4c8a

File tree

7 files changed

+163
-6
lines changed

7 files changed

+163
-6
lines changed

Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

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

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", from: "0.15.0"),
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: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import Foundation
2+
import GRDB
3+
import GRDBSQLite
4+
5+
extension Database {
6+
/// Adds a user-defined `@DatabaseFunction` to a connection.
7+
///
8+
/// - Parameter function: A database function to add.
9+
public func add(function: some ScalarDatabaseFunction) {
10+
sqlite3_create_function_v2(
11+
sqliteConnection,
12+
function.name,
13+
function.argumentCount,
14+
function.textEncoding,
15+
Unmanaged.passRetained(ScalarDatabaseFunctionBox(function)).toOpaque(),
16+
{ context, argumentCount, arguments in
17+
Unmanaged<ScalarDatabaseFunctionBox>
18+
.fromOpaque(sqlite3_user_data(context))
19+
.takeUnretainedValue()
20+
.function
21+
.invoke([QueryBinding](argumentCount: argumentCount, arguments: arguments))
22+
.result(db: context)
23+
},
24+
nil,
25+
nil,
26+
{ box in
27+
guard let box else { return }
28+
Unmanaged<ScalarDatabaseFunctionBox>.fromOpaque(box).release()
29+
}
30+
)
31+
}
32+
33+
/// Deletes a user-defined `@DatabaseFunction` from a connection.
34+
///
35+
/// - Parameter function: A database function to delete.
36+
public func remove(function: some ScalarDatabaseFunction) {
37+
sqlite3_create_function_v2(
38+
sqliteConnection,
39+
function.name,
40+
function.argumentCount,
41+
function.textEncoding,
42+
nil,
43+
nil,
44+
nil,
45+
nil,
46+
nil
47+
)
48+
}
49+
}
50+
51+
extension ScalarDatabaseFunction {
52+
fileprivate var argumentCount: Int32 {
53+
Int32(argumentCount ?? -1)
54+
}
55+
56+
fileprivate var textEncoding: Int32 {
57+
SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0)
58+
}
59+
}
60+
61+
private final class ScalarDatabaseFunctionBox {
62+
let function: any ScalarDatabaseFunction
63+
init(_ function: some ScalarDatabaseFunction) {
64+
self.function = function
65+
}
66+
}
67+
68+
extension [QueryBinding] {
69+
fileprivate init(argumentCount: Int32, arguments: UnsafeMutablePointer<OpaquePointer?>?) {
70+
self = (0..<argumentCount).map { offset in
71+
let value = arguments?[Int(offset)]
72+
switch sqlite3_value_type(value) {
73+
case SQLITE_BLOB:
74+
if let blob = sqlite3_value_blob(value) {
75+
let count = Int(sqlite3_value_bytes(value))
76+
let buffer = UnsafeRawBufferPointer(start: blob, count: count)
77+
return .blob([UInt8](buffer))
78+
} else {
79+
return .blob([])
80+
}
81+
case SQLITE_FLOAT:
82+
return .double(sqlite3_value_double(value))
83+
case SQLITE_INTEGER:
84+
return .int(sqlite3_value_int64(value))
85+
case SQLITE_NULL:
86+
return .null
87+
case SQLITE_TEXT:
88+
return .text(String(cString: UnsafePointer(sqlite3_value_text(value))))
89+
default:
90+
return .invalid(UnknownType())
91+
}
92+
}
93+
}
94+
95+
private struct UnknownType: Error {}
96+
}
97+
98+
extension QueryBinding {
99+
fileprivate func result(db: OpaquePointer?) {
100+
switch self {
101+
case .blob(let value):
102+
sqlite3_result_blob(db, Array(value), Int32(value.count), SQLITE_TRANSIENT)
103+
case .double(let value):
104+
sqlite3_result_double(db, value)
105+
case .date(let value):
106+
sqlite3_result_text(db, value.iso8601String, -1, SQLITE_TRANSIENT)
107+
case .int(let value):
108+
sqlite3_result_int64(db, value)
109+
case .null:
110+
sqlite3_result_null(db)
111+
case .text(let value):
112+
sqlite3_result_text(db, value, -1, SQLITE_TRANSIENT)
113+
case .uuid(let value):
114+
sqlite3_result_text(db, value.uuidString.lowercased(), -1, SQLITE_TRANSIENT)
115+
case .invalid(let error):
116+
sqlite3_result_error(db, error.underlyingError.localizedDescription, -1)
117+
}
118+
}
119+
}
120+
121+
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
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Foundation
2+
import GRDB
3+
import StructuredQueriesGRDB
4+
import Testing
5+
6+
@Suite struct CustomFunctionsTests {
7+
@DatabaseFunction func customDate() -> Date {
8+
Date(timeIntervalSinceReferenceDate: 0)
9+
}
10+
11+
@Test func basics() throws {
12+
var configuration = Configuration()
13+
configuration.prepareDatabase { db in
14+
db.add(function: $customDate)
15+
}
16+
let database = try DatabaseQueue(configuration: configuration)
17+
let date = try database.read { db in
18+
try Values($customDate())
19+
.fetchOne(db)
20+
}
21+
#expect(date?.timeIntervalSinceReferenceDate == 0)
22+
23+
try database.write { db in
24+
db.remove(function: $customDate)
25+
}
26+
#expect(throws: (any Error).self) {
27+
try database.read { db in
28+
_ = try Values($customDate()).fetchOne(db)
29+
}
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)