From d7f73e27ffe2aa16e2b7bb00ef00ab33c1f798dd Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 10:23:03 -0700 Subject: [PATCH 1/6] Support for StructuredQueries' `@DatabaseFunction` macro See https://github.com/pointfreeco/swift-structured-queries/pull/151 for more details. --- Package.resolved | 6 +- Package.swift | 4 +- .../StructuredQueriesGRDB.swift | 1 + .../CustomFunctions.swift | 122 ++++++++++++++++++ .../Internal/Exports.swift | 1 + 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 Sources/StructuredQueriesGRDBCore/CustomFunctions.swift diff --git a/Package.resolved b/Package.resolved index a4e00c4e..2894e8d4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d87ac3bfcdef05674d1d54c62277ab77a7f1a74d2b106a3e0a8eb5567e2ff1ed", + "originHash" : "1932408fc9a59e0236b9a7e2c6038962acd4027e2acea33c798d3e20a50fbf02", "pins" : [ { "identity" : "combine-schedulers", @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "b5b5a9ed9ff321f43a02f07394f2831eba5e11f2", - "version" : "0.13.0" + "branch" : "custom-functions", + "revision" : "038b40be3b89d1794854b48bfc942fd09811885e" } }, { diff --git a/Package.swift b/Package.swift index d389be9e..4b2186f0 100644 --- a/Package.swift +++ b/Package.swift @@ -39,7 +39,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.13.0"), + .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "custom-functions"), ], targets: [ .target( @@ -82,6 +82,7 @@ let package = Package( .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), .product(name: "StructuredQueriesCore", package: "swift-structured-queries"), + .product(name: "StructuredQueriesSQLiteCore", package: "swift-structured-queries"), ] ), .target( @@ -89,6 +90,7 @@ let package = Package( dependencies: [ "StructuredQueriesGRDBCore", .product(name: "StructuredQueries", package: "swift-structured-queries"), + .product(name: "StructuredQueriesSQLite", package: "swift-structured-queries"), ] ), .testTarget( diff --git a/Sources/StructuredQueriesGRDB/StructuredQueriesGRDB.swift b/Sources/StructuredQueriesGRDB/StructuredQueriesGRDB.swift index 99a11a9d..047e9bfe 100644 --- a/Sources/StructuredQueriesGRDB/StructuredQueriesGRDB.swift +++ b/Sources/StructuredQueriesGRDB/StructuredQueriesGRDB.swift @@ -1,2 +1,3 @@ @_exported import StructuredQueries +@_exported import StructuredQueriesSQLite @_exported import StructuredQueriesGRDBCore diff --git a/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift b/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift new file mode 100644 index 00000000..46a3e7aa --- /dev/null +++ b/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift @@ -0,0 +1,122 @@ +import GRDB +import GRDBSQLite +import Foundation + +extension ScalarDatabaseFunction { + /// Adds a user-defined `@DatabaseFunction` to a connection. + /// + /// - Parameter db: A database connection. + public func install(db: Database) { + let db = db.sqliteConnection + let box = Unmanaged.passRetained(ScalarDatabaseFunctionBox(self)).toOpaque() + sqlite3_create_function_v2( + db, + name, + argumentCount, + textEncoding, + box, + { context, argumentCount, arguments in + Unmanaged + .fromOpaque(sqlite3_user_data(context)) + .takeUnretainedValue() + .function + .invoke([QueryBinding](argumentCount: argumentCount, arguments: arguments)) + .result(db: context) + }, + nil, + nil, + { context in + guard let context else { return } + Unmanaged.fromOpaque(context).release() + } + ) + } + + /// Deletes a user-defined `@DatabaseFunction` from a connection. + /// + /// - Parameter db: A database connection. + public func uninstall(db: Database) { + let db = db.sqliteConnection + sqlite3_create_function_v2( + db, + name, + argumentCount, + textEncoding, + nil, + nil, + nil, + nil, + nil + ) + } + + private var argumentCount: Int32 { + Int32(argumentCount ?? -1) + } + + private var textEncoding: Int32 { + SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0) + } +} + +private final class ScalarDatabaseFunctionBox { + let function: any ScalarDatabaseFunction + init(_ function: some ScalarDatabaseFunction) { + self.function = function + } +} + +extension [QueryBinding] { + fileprivate init(argumentCount: Int32, arguments: UnsafeMutablePointer?) { + self = (0.. Date: Mon, 1 Sep 2025 10:56:42 -0700 Subject: [PATCH 2/6] wip --- .../CustomFunctions.swift | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift b/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift index 46a3e7aa..0e946823 100644 --- a/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift +++ b/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift @@ -2,18 +2,17 @@ import GRDB import GRDBSQLite import Foundation -extension ScalarDatabaseFunction { +extension Database { /// Adds a user-defined `@DatabaseFunction` to a connection. /// - /// - Parameter db: A database connection. - public func install(db: Database) { - let db = db.sqliteConnection - let box = Unmanaged.passRetained(ScalarDatabaseFunctionBox(self)).toOpaque() + /// - Parameter function: A database function to add. + public func add(function: some ScalarDatabaseFunction) { + let box = Unmanaged.passRetained(ScalarDatabaseFunctionBox(function)).toOpaque() sqlite3_create_function_v2( - db, - name, - argumentCount, - textEncoding, + sqliteConnection, + function.name, + function.argumentCount, + function.textEncoding, box, { context, argumentCount, arguments in Unmanaged @@ -25,23 +24,22 @@ extension ScalarDatabaseFunction { }, nil, nil, - { context in - guard let context else { return } - Unmanaged.fromOpaque(context).release() + { box in + guard let box else { return } + Unmanaged.fromOpaque(box).release() } ) } /// Deletes a user-defined `@DatabaseFunction` from a connection. /// - /// - Parameter db: A database connection. - public func uninstall(db: Database) { - let db = db.sqliteConnection + /// - Parameter function: A database function to delete. + public func remove(function: some ScalarDatabaseFunction) { sqlite3_create_function_v2( - db, - name, - argumentCount, - textEncoding, + sqliteConnection, + function.name, + function.argumentCount, + function.textEncoding, nil, nil, nil, @@ -49,12 +47,14 @@ extension ScalarDatabaseFunction { nil ) } +} - private var argumentCount: Int32 { +extension ScalarDatabaseFunction { + fileprivate var argumentCount: Int32 { Int32(argumentCount ?? -1) } - private var textEncoding: Int32 { + fileprivate var textEncoding: Int32 { SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0) } } From b1d05925570dcb830db66f029f7b8730e7dc4220 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 10:57:46 -0700 Subject: [PATCH 3/6] wip --- Sources/StructuredQueriesGRDBCore/CustomFunctions.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift b/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift index 0e946823..38c7b7a5 100644 --- a/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift +++ b/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift @@ -7,13 +7,12 @@ extension Database { /// /// - Parameter function: A database function to add. public func add(function: some ScalarDatabaseFunction) { - let box = Unmanaged.passRetained(ScalarDatabaseFunctionBox(function)).toOpaque() sqlite3_create_function_v2( sqliteConnection, function.name, function.argumentCount, function.textEncoding, - box, + Unmanaged.passRetained(ScalarDatabaseFunctionBox(function)).toOpaque(), { context, argumentCount, arguments in Unmanaged .fromOpaque(sqlite3_user_data(context)) From af10454f028ed690527c4ae0e6eb5dc72b7a9d97 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 11:02:23 -0700 Subject: [PATCH 4/6] test --- .../CustomFunctionTests.swift | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Tests/StructuredQueriesGRDBTests/CustomFunctionTests.swift diff --git a/Tests/StructuredQueriesGRDBTests/CustomFunctionTests.swift b/Tests/StructuredQueriesGRDBTests/CustomFunctionTests.swift new file mode 100644 index 00000000..003e3370 --- /dev/null +++ b/Tests/StructuredQueriesGRDBTests/CustomFunctionTests.swift @@ -0,0 +1,32 @@ +import Foundation +import GRDB +import StructuredQueriesGRDB +import Testing + +@Suite struct CustomFunctionsTests { + @DatabaseFunction func customDate() -> Date { + Date(timeIntervalSinceReferenceDate: 0) + } + + @Test func basics() throws { + var configuration = Configuration() + configuration.prepareDatabase { db in + db.add(function: $customDate) + } + let database = try DatabaseQueue(configuration: configuration) + let date = try database.read { db in + try Values($customDate()) + .fetchOne(db) + } + #expect(date?.timeIntervalSinceReferenceDate == 0) + + try database.write { db in + db.remove(function: $customDate) + } + #expect(throws: (any Error).self) { + try database.read { db in + _ = try Values($customDate()).fetchOne(db) + } + } + } +} From bbb0323afc33d02afbcb4fb9d56b78f060325527 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 11:02:58 -0700 Subject: [PATCH 5/6] wip --- Sources/StructuredQueriesGRDBCore/CustomFunctions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift b/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift index 38c7b7a5..2b93a3f2 100644 --- a/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift +++ b/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift @@ -1,6 +1,6 @@ +import Foundation import GRDB import GRDBSQLite -import Foundation extension Database { /// Adds a user-defined `@DatabaseFunction` to a connection. From b3e837114dd056c1f48065b451702f565c1d9d79 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 15:25:36 -0700 Subject: [PATCH 6/6] wip --- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- Package.resolved | 6 +++--- Package.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c35f7919..bf1fd701 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "b5b5a9ed9ff321f43a02f07394f2831eba5e11f2", - "version" : "0.13.0" + "revision" : "e53cd5c87b31eeba77a801eec9a7e82c6d7faaab", + "version" : "0.15.0" } }, { diff --git a/Package.resolved b/Package.resolved index 2894e8d4..a28c3596 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "1932408fc9a59e0236b9a7e2c6038962acd4027e2acea33c798d3e20a50fbf02", + "originHash" : "96fc55dd49af697ce0e0bf306c46a68e5c589a1cabb04749621327771c159fb5", "pins" : [ { "identity" : "combine-schedulers", @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "branch" : "custom-functions", - "revision" : "038b40be3b89d1794854b48bfc942fd09811885e" + "revision" : "e53cd5c87b31eeba77a801eec9a7e82c6d7faaab", + "version" : "0.15.0" } }, { diff --git a/Package.swift b/Package.swift index 4b2186f0..41c5f830 100644 --- a/Package.swift +++ b/Package.swift @@ -39,7 +39,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", branch: "custom-functions"), + .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.15.0"), ], targets: [ .target(