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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -82,13 +82,15 @@ 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(
name: "StructuredQueriesGRDB",
dependencies: [
"StructuredQueriesGRDBCore",
.product(name: "StructuredQueries", package: "swift-structured-queries"),
.product(name: "StructuredQueriesSQLite", package: "swift-structured-queries"),
]
),
.testTarget(
Expand Down
1 change: 1 addition & 0 deletions Sources/StructuredQueriesGRDB/StructuredQueriesGRDB.swift
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@_exported import StructuredQueries
@_exported import StructuredQueriesSQLite
@_exported import StructuredQueriesGRDBCore
121 changes: 121 additions & 0 deletions Sources/StructuredQueriesGRDBCore/CustomFunctions.swift
Original file line number Diff line number Diff line change
@@ -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<ScalarDatabaseFunctionBox>
.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<ScalarDatabaseFunctionBox>.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<OpaquePointer?>?) {
self = (0..<argumentCount).map { offset in
let value = arguments?[Int(offset)]
switch sqlite3_value_type(value) {
case SQLITE_BLOB:
if let blob = sqlite3_value_blob(value) {
let count = Int(sqlite3_value_bytes(value))
let buffer = UnsafeRawBufferPointer(start: blob, count: count)
return .blob([UInt8](buffer))
} else {
return .blob([])
}
case SQLITE_FLOAT:
return .double(sqlite3_value_double(value))
case SQLITE_INTEGER:
return .int(sqlite3_value_int64(value))
case SQLITE_NULL:
return .null
case SQLITE_TEXT:
return .text(String(cString: UnsafePointer(sqlite3_value_text(value))))
default:
return .invalid(UnknownType())
}
}
}

private struct UnknownType: Error {}
}

extension QueryBinding {
fileprivate func result(db: OpaquePointer?) {
switch self {
case .blob(let value):
sqlite3_result_blob(db, Array(value), Int32(value.count), SQLITE_TRANSIENT)
case .double(let value):
sqlite3_result_double(db, value)
case .date(let value):
sqlite3_result_text(db, value.iso8601String, -1, SQLITE_TRANSIENT)
case .int(let value):
sqlite3_result_int64(db, value)
case .null:
sqlite3_result_null(db)
case .text(let value):
sqlite3_result_text(db, value, -1, SQLITE_TRANSIENT)
case .uuid(let value):
sqlite3_result_text(db, value.uuidString.lowercased(), -1, SQLITE_TRANSIENT)
case .invalid(let error):
sqlite3_result_error(db, error.underlyingError.localizedDescription, -1)
}
}
}

let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
1 change: 1 addition & 0 deletions Sources/StructuredQueriesGRDBCore/Internal/Exports.swift
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@_exported import StructuredQueriesCore
@_exported import StructuredQueriesSQLiteCore
32 changes: 32 additions & 0 deletions Tests/StructuredQueriesGRDBTests/CustomFunctionTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}