483 lines
18 KiB
Swift
483 lines
18 KiB
Swift
/// A custom SQL function or aggregate.
|
|
///
|
|
/// ## Topics
|
|
///
|
|
/// ### Creating a Custom SQL Function or Aggregate
|
|
///
|
|
/// - ``init(_:argumentCount:pure:function:)``
|
|
/// - ``init(_:argumentCount:pure:aggregate:)``
|
|
/// - ``DatabaseAggregate``
|
|
///
|
|
/// ### Calling an SQL Function or Aggregate
|
|
///
|
|
/// - ``callAsFunction(_:)``
|
|
///
|
|
/// ### Built-in Functions
|
|
///
|
|
/// - ``capitalize``
|
|
/// - ``localizedCapitalize``
|
|
/// - ``localizedLowercase``
|
|
/// - ``localizedUppercase``
|
|
/// - ``lowercase``
|
|
/// - ``uppercase``
|
|
public final class DatabaseFunction: Hashable {
|
|
// SQLite identifies functions by (name + argument count)
|
|
private struct Identity: Hashable {
|
|
let name: String
|
|
let nArg: CInt // -1 for variadic functions
|
|
}
|
|
|
|
/// The name of the SQL function
|
|
public var name: String { identity.name }
|
|
private let identity: Identity
|
|
let isPure: Bool
|
|
private let kind: Kind
|
|
private var eTextRep: CInt { (SQLITE_UTF8 | (isPure ? SQLITE_DETERMINISTIC : 0)) }
|
|
|
|
/// Creates an SQL function.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let succ = DatabaseFunction("succ", argumentCount: 1) { dbValues in
|
|
/// guard let int = Int.fromDatabaseValue(dbValues[0]) else {
|
|
/// return nil
|
|
/// }
|
|
/// return int + 1
|
|
/// }
|
|
/// let dbQueue = try DatabaseQueue()
|
|
/// try dbQueue.read { db in
|
|
/// db.add(function: succ)
|
|
/// try Int.fetchOne(db, sql: "SELECT succ(1)")! // 2
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// ### Related APIs
|
|
///
|
|
/// - ``Database/add(function:)``
|
|
///
|
|
/// - parameters:
|
|
/// - name: The function name.
|
|
/// - argumentCount: The number of arguments of the function. If
|
|
/// omitted, or nil, the function accepts any number of arguments.
|
|
/// - pure: Whether the function is "pure", which means that its results
|
|
/// only depends on its inputs. When a function is pure, SQLite has
|
|
/// the opportunity to perform additional optimizations. Default value
|
|
/// is false.
|
|
/// - function: A function that takes an array of ``DatabaseValue``
|
|
/// arguments, and returns an optional ``DatabaseValueConvertible``
|
|
/// such as `Int`, `String`, `Date`, etc. The array is guaranteed to
|
|
/// have exactly `argumentCount` elements, provided `argumentCount` is
|
|
/// not nil.
|
|
public init(
|
|
_ name: String,
|
|
argumentCount: Int? = nil,
|
|
pure: Bool = false,
|
|
function: @escaping ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?)
|
|
{
|
|
self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
|
|
self.isPure = pure
|
|
self.kind = .function{ (argc, argv) in
|
|
let arguments = (0..<Int(argc)).map { index in
|
|
DatabaseValue(sqliteValue: argv.unsafelyUnwrapped[index]!)
|
|
}
|
|
return try function(arguments)
|
|
}
|
|
}
|
|
|
|
/// Creates an SQL aggregate function.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct MySum: DatabaseAggregate {
|
|
/// var sum: Int = 0
|
|
///
|
|
/// mutating func step(_ dbValues: [DatabaseValue]) {
|
|
/// if let int = Int.fromDatabaseValue(dbValues[0]) {
|
|
/// sum += int
|
|
/// }
|
|
/// }
|
|
///
|
|
/// func finalize() -> (any DatabaseValueConvertible)? {
|
|
/// return sum
|
|
/// }
|
|
/// }
|
|
///
|
|
/// let dbQueue = try DatabaseQueue()
|
|
/// let mySum = DatabaseFunction("mySum", argumentCount: 1, aggregate: MySum.self)
|
|
/// try dbQueue.write { db in
|
|
/// db.add(function: mySum)
|
|
/// try db.execute(sql: "CREATE TABLE test(i)")
|
|
/// try db.execute(sql: "INSERT INTO test(i) VALUES (1)")
|
|
/// try db.execute(sql: "INSERT INTO test(i) VALUES (2)")
|
|
/// try Int.fetchOne(db, sql: "SELECT mySum(i) FROM test")! // 3
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// ### Related APIs
|
|
///
|
|
/// - ``Database/add(function:)``
|
|
///
|
|
/// - parameters:
|
|
/// - name: The function name.
|
|
/// - argumentCount: The number of arguments of the aggregate. If
|
|
/// omitted, or nil, the aggregate accepts any number of arguments.
|
|
/// - pure: Whether the aggregate is "pure", which means that its
|
|
/// results only depends on its inputs. When an aggregate is pure,
|
|
/// SQLite has the opportunity to perform additional optimizations.
|
|
/// Default value is false.
|
|
/// - aggregate: A type that implements the ``DatabaseAggregate``
|
|
/// protocol. For each step of the aggregation, its
|
|
/// ``DatabaseAggregate/step(_:)`` method is called with an array of
|
|
/// ``DatabaseValue`` arguments. The array is guaranteed to have
|
|
/// exactly `argumentCount` elements, provided `argumentCount` is
|
|
/// not nil.
|
|
public init<Aggregate: DatabaseAggregate>(
|
|
_ name: String,
|
|
argumentCount: Int? = nil,
|
|
pure: Bool = false,
|
|
aggregate: Aggregate.Type)
|
|
{
|
|
self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
|
|
self.isPure = pure
|
|
self.kind = .aggregate { Aggregate() }
|
|
}
|
|
|
|
// TODO: GRDB7 -> expose ORDER BY and FILTER when we have distinct types for simple functions and aggregates.
|
|
/// Returns an SQL expression that applies the function.
|
|
///
|
|
/// You can use a `DatabaseFunction` as a regular Swift function. It returns
|
|
/// an SQL expression that you can use in the query interface.
|
|
///
|
|
/// In the example below, `square(Column("score"))` generates the
|
|
/// `square(score)` SQL expression:
|
|
///
|
|
/// ```swift
|
|
/// let square = DatabaseFunction("square", argumentCount: 1) { dbValues in
|
|
/// guard let int = Int.fromDatabaseValue(dbValues[0]) else {
|
|
/// return nil
|
|
/// }
|
|
/// return int * int
|
|
/// }
|
|
/// let dbQueue = try DatabaseQueue()
|
|
/// try dbQueue.read { db in
|
|
/// db.add(function: square)
|
|
///
|
|
/// // SELECT square(score) FROM player
|
|
/// let squaredScores = let Player
|
|
/// .select(square(Column("score")), as: Int.self)
|
|
/// .fetchAll(db)
|
|
/// }
|
|
/// ```
|
|
public func callAsFunction(_ arguments: any SQLExpressible...) -> SQLExpression {
|
|
switch kind {
|
|
case .function:
|
|
return .simpleFunction(
|
|
name,
|
|
arguments.map(\.sqlExpression),
|
|
isPure: isPure,
|
|
isJSONValue: false)
|
|
case .aggregate:
|
|
return .aggregateFunction(
|
|
name,
|
|
arguments.map(\.sqlExpression),
|
|
isDistinct: false,
|
|
ordering: nil,
|
|
filter: nil,
|
|
isJSONValue: false)
|
|
}
|
|
}
|
|
|
|
/// Calls sqlite3_create_function_v2
|
|
/// See <https://sqlite.org/c3ref/create_function.html>
|
|
func install(in db: Database) {
|
|
// Retain the function definition
|
|
let definition = kind.definition
|
|
let definitionP = Unmanaged.passRetained(definition).toOpaque()
|
|
|
|
let code = sqlite3_create_function_v2(
|
|
db.sqliteConnection,
|
|
identity.name,
|
|
identity.nArg,
|
|
eTextRep,
|
|
definitionP,
|
|
kind.xFunc,
|
|
kind.xStep,
|
|
kind.xFinal,
|
|
{ definitionP in
|
|
// Release the function definition
|
|
Unmanaged<AnyObject>.fromOpaque(definitionP!).release()
|
|
})
|
|
|
|
guard code == SQLITE_OK else {
|
|
// Assume a GRDB bug: there is no point throwing any error.
|
|
fatalError(DatabaseError(resultCode: code, message: db.lastErrorMessage))
|
|
}
|
|
}
|
|
|
|
/// Calls sqlite3_create_function_v2
|
|
/// See <https://sqlite.org/c3ref/create_function.html>
|
|
func uninstall(in db: Database) {
|
|
let code = sqlite3_create_function_v2(
|
|
db.sqliteConnection,
|
|
identity.name,
|
|
identity.nArg,
|
|
eTextRep,
|
|
nil, nil, nil, nil, nil)
|
|
|
|
guard code == SQLITE_OK else {
|
|
// Assume a GRDB bug: there is no point throwing any error.
|
|
fatalError(DatabaseError(resultCode: code, message: db.lastErrorMessage))
|
|
}
|
|
}
|
|
|
|
/// The way to compute the result of a function.
|
|
/// Feeds the `pApp` parameter of sqlite3_create_function_v2
|
|
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
|
private class FunctionDefinition {
|
|
let compute: (CInt, UnsafeMutablePointer<OpaquePointer?>?) throws -> (any DatabaseValueConvertible)?
|
|
init(compute: @escaping (CInt, UnsafeMutablePointer<OpaquePointer?>?)
|
|
throws -> (any DatabaseValueConvertible)?)
|
|
{
|
|
self.compute = compute
|
|
}
|
|
}
|
|
|
|
/// The way to start an aggregate.
|
|
/// Feeds the `pApp` parameter of sqlite3_create_function_v2
|
|
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
|
private class AggregateDefinition {
|
|
let makeAggregate: () -> any DatabaseAggregate
|
|
init(makeAggregate: @escaping () -> any DatabaseAggregate) {
|
|
self.makeAggregate = makeAggregate
|
|
}
|
|
}
|
|
|
|
/// The current state of an aggregate, storable in SQLite
|
|
private class AggregateContext {
|
|
var aggregate: any DatabaseAggregate
|
|
var hasErrored = false
|
|
init(aggregate: some DatabaseAggregate) {
|
|
self.aggregate = aggregate
|
|
}
|
|
}
|
|
|
|
/// A function kind: an "SQL function" or an "aggregate".
|
|
/// See <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
|
private enum Kind {
|
|
/// A regular function: SELECT f(1)
|
|
case function((CInt, UnsafeMutablePointer<OpaquePointer?>?) throws -> (any DatabaseValueConvertible)?)
|
|
|
|
/// An aggregate: SELECT f(foo) FROM bar GROUP BY baz
|
|
case aggregate(() -> any DatabaseAggregate)
|
|
|
|
/// Feeds the `pApp` parameter of sqlite3_create_function_v2
|
|
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
|
var definition: AnyObject {
|
|
switch self {
|
|
case .function(let compute):
|
|
return FunctionDefinition(compute: compute)
|
|
case .aggregate(let makeAggregate):
|
|
return AggregateDefinition(makeAggregate: makeAggregate)
|
|
}
|
|
}
|
|
|
|
/// Feeds the `xFunc` parameter of sqlite3_create_function_v2
|
|
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
|
var xFunc: (@convention(c) (OpaquePointer?, CInt, UnsafeMutablePointer<OpaquePointer?>?) -> Void)? {
|
|
guard case .function = self else { return nil }
|
|
return { (sqliteContext, argc, argv) in
|
|
let definition = Unmanaged<FunctionDefinition>
|
|
.fromOpaque(sqlite3_user_data(sqliteContext))
|
|
.takeUnretainedValue()
|
|
do {
|
|
try DatabaseFunction.report(
|
|
result: definition.compute(argc, argv),
|
|
in: sqliteContext)
|
|
} catch {
|
|
DatabaseFunction.report(error: error, in: sqliteContext)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Feeds the `xStep` parameter of sqlite3_create_function_v2
|
|
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
|
var xStep: (@convention(c) (OpaquePointer?, CInt, UnsafeMutablePointer<OpaquePointer?>?) -> Void)? {
|
|
guard case .aggregate = self else { return nil }
|
|
return { (sqliteContext, argc, argv) in
|
|
let aggregateContextU = DatabaseFunction.unmanagedAggregateContext(sqliteContext)
|
|
let aggregateContext = aggregateContextU.takeUnretainedValue()
|
|
assert(!aggregateContext.hasErrored) // assert SQLite behavior
|
|
do {
|
|
let arguments = (0..<Int(argc)).map { index in
|
|
DatabaseValue(sqliteValue: argv.unsafelyUnwrapped[index]!)
|
|
}
|
|
try aggregateContext.aggregate.step(arguments)
|
|
} catch {
|
|
aggregateContext.hasErrored = true
|
|
DatabaseFunction.report(error: error, in: sqliteContext)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Feeds the `xFinal` parameter of sqlite3_create_function_v2
|
|
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
|
var xFinal: (@convention(c) (OpaquePointer?) -> Void)? {
|
|
guard case .aggregate = self else { return nil }
|
|
return { (sqliteContext) in
|
|
let aggregateContextU = DatabaseFunction.unmanagedAggregateContext(sqliteContext)
|
|
let aggregateContext = aggregateContextU.takeUnretainedValue()
|
|
aggregateContextU.release()
|
|
|
|
guard !aggregateContext.hasErrored else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
try DatabaseFunction.report(
|
|
result: aggregateContext.aggregate.finalize(),
|
|
in: sqliteContext)
|
|
} catch {
|
|
DatabaseFunction.report(error: error, in: sqliteContext)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper function that extracts the current state of an aggregate from an
|
|
/// sqlite function execution context.
|
|
///
|
|
/// The result must be released when the aggregate concludes.
|
|
///
|
|
/// See <https://sqlite.org/c3ref/context.html>
|
|
/// See <https://sqlite.org/c3ref/aggregate_context.html>
|
|
private static func unmanagedAggregateContext(_ sqliteContext: OpaquePointer?) -> Unmanaged<AggregateContext> {
|
|
// > The first time the sqlite3_aggregate_context(C,N) routine is called
|
|
// > for a particular aggregate function, SQLite allocates N of memory,
|
|
// > zeroes out that memory, and returns a pointer to the new memory.
|
|
// > On second and subsequent calls to sqlite3_aggregate_context() for
|
|
// > the same aggregate function instance, the same buffer is returned.
|
|
let stride = MemoryLayout<Unmanaged<AggregateContext>>.stride
|
|
let aggregateContextBufferP = UnsafeMutableRawBufferPointer(
|
|
start: sqlite3_aggregate_context(sqliteContext, CInt(stride))!,
|
|
count: stride)
|
|
|
|
if aggregateContextBufferP.contains(where: { $0 != 0 }) {
|
|
// Buffer contains non-zero byte: load aggregate context
|
|
let aggregateContextP = aggregateContextBufferP
|
|
.baseAddress!
|
|
.assumingMemoryBound(to: Unmanaged<AggregateContext>.self)
|
|
return aggregateContextP.pointee
|
|
} else {
|
|
// Buffer contains null pointer: create aggregate context.
|
|
let aggregate = Unmanaged<AggregateDefinition>.fromOpaque(sqlite3_user_data(sqliteContext))
|
|
.takeUnretainedValue()
|
|
.makeAggregate()
|
|
let aggregateContext = AggregateContext(aggregate: aggregate)
|
|
|
|
// retain and store in SQLite's buffer
|
|
let aggregateContextU = Unmanaged.passRetained(aggregateContext)
|
|
let aggregateContextP = aggregateContextU.toOpaque()
|
|
withUnsafeBytes(of: aggregateContextP) {
|
|
aggregateContextBufferP.copyMemory(from: $0)
|
|
}
|
|
return aggregateContextU
|
|
}
|
|
}
|
|
|
|
private static func report(result: (any DatabaseValueConvertible)?, in sqliteContext: OpaquePointer?) {
|
|
switch result?.databaseValue.storage ?? .null {
|
|
case .null:
|
|
sqlite3_result_null(sqliteContext)
|
|
case .int64(let int64):
|
|
sqlite3_result_int64(sqliteContext, int64)
|
|
case .double(let double):
|
|
sqlite3_result_double(sqliteContext, double)
|
|
case .string(let string):
|
|
sqlite3_result_text(sqliteContext, string, -1, SQLITE_TRANSIENT)
|
|
case .blob(let data):
|
|
data.withUnsafeBytes {
|
|
sqlite3_result_blob(sqliteContext, $0.baseAddress, CInt($0.count), SQLITE_TRANSIENT)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func report(error: Error, in sqliteContext: OpaquePointer?) {
|
|
if let error = error as? DatabaseError {
|
|
if let message = error.message {
|
|
sqlite3_result_error(sqliteContext, message, -1)
|
|
}
|
|
sqlite3_result_error_code(sqliteContext, error.extendedResultCode.rawValue)
|
|
} else {
|
|
sqlite3_result_error(sqliteContext, "\(error)", -1)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DatabaseFunction {
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(identity)
|
|
}
|
|
|
|
/// Two functions are equal if they share the same name and arity.
|
|
public static func == (lhs: DatabaseFunction, rhs: DatabaseFunction) -> Bool {
|
|
lhs.identity == rhs.identity
|
|
}
|
|
}
|
|
|
|
/// The protocol for custom SQLite aggregates.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct MySum : DatabaseAggregate {
|
|
/// var sum: Int = 0
|
|
///
|
|
/// mutating func step(_ dbValues: [DatabaseValue]) {
|
|
/// if let int = Int.fromDatabaseValue(dbValues[0]) {
|
|
/// sum += int
|
|
/// }
|
|
/// }
|
|
///
|
|
/// func finalize() -> (any DatabaseValueConvertible)? {
|
|
/// return sum
|
|
/// }
|
|
/// }
|
|
///
|
|
/// let dbQueue = try DatabaseQueue()
|
|
/// let mySum = DatabaseFunction("mySum", argumentCount: 1, aggregate: MySum.self)
|
|
/// try dbQueue.write { db in
|
|
/// db.add(function: mySum)
|
|
/// try db.execute(sql: "CREATE TABLE test(i)")
|
|
/// try db.execute(sql: "INSERT INTO test(i) VALUES (1)")
|
|
/// try db.execute(sql: "INSERT INTO test(i) VALUES (2)")
|
|
/// try Int.fetchOne(db, sql: "SELECT mysum(i) FROM test")! // 3
|
|
/// }
|
|
/// ```
|
|
public protocol DatabaseAggregate {
|
|
/// Creates an aggregate.
|
|
///
|
|
/// A new instance is created for each aggregation.
|
|
init()
|
|
|
|
/// Updates the aggregated value for one step of the aggregation.
|
|
///
|
|
/// This method is called once for each step of the aggregation.
|
|
///
|
|
/// The `dbValues` argument contains as many values as given to the SQL
|
|
/// aggregate function:
|
|
///
|
|
/// ```sql
|
|
/// -- One value
|
|
/// SELECT maxLength(name) FROM player
|
|
///
|
|
/// -- Two values
|
|
/// SELECT maxFullNameLength(firstName, lastName) FROM player
|
|
/// ```
|
|
mutating func step(_ dbValues: [DatabaseValue]) throws
|
|
|
|
/// Returns the aggregated value.
|
|
func finalize() throws -> (any DatabaseValueConvertible)?
|
|
}
|