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 a4e00c4e..a28c3596 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d87ac3bfcdef05674d1d54c62277ab77a7f1a74d2b106a3e0a8eb5567e2ff1ed", + "originHash" : "96fc55dd49af697ce0e0bf306c46a68e5c589a1cabb04749621327771c159fb5", "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" + "revision" : "e53cd5c87b31eeba77a801eec9a7e82c6d7faaab", + "version" : "0.15.0" } }, { diff --git a/Package.swift b/Package.swift index d389be9e..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", from: "0.13.0"), + .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.15.0"), ], 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..2b93a3f2 --- /dev/null +++ b/Sources/StructuredQueriesGRDBCore/CustomFunctions.swift @@ -0,0 +1,121 @@ +import Foundation +import GRDB +import GRDBSQLite + +extension Database { + /// Adds a user-defined `@DatabaseFunction` to a connection. + /// + /// - Parameter function: A database function to add. + public func add(function: some ScalarDatabaseFunction) { + sqlite3_create_function_v2( + sqliteConnection, + function.name, + function.argumentCount, + function.textEncoding, + Unmanaged.passRetained(ScalarDatabaseFunctionBox(function)).toOpaque(), + { context, argumentCount, arguments in + Unmanaged + .fromOpaque(sqlite3_user_data(context)) + .takeUnretainedValue() + .function + .invoke([QueryBinding](argumentCount: argumentCount, arguments: arguments)) + .result(db: context) + }, + nil, + nil, + { box in + guard let box else { return } + Unmanaged.fromOpaque(box).release() + } + ) + } + + /// Deletes a user-defined `@DatabaseFunction` from a connection. + /// + /// - Parameter function: A database function to delete. + public func remove(function: some ScalarDatabaseFunction) { + sqlite3_create_function_v2( + sqliteConnection, + function.name, + function.argumentCount, + function.textEncoding, + nil, + nil, + nil, + nil, + nil + ) + } +} + +extension ScalarDatabaseFunction { + fileprivate var argumentCount: Int32 { + Int32(argumentCount ?? -1) + } + + fileprivate 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 { + 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) + } + } + } +}