Add CI/CD configuration and API documentation
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
/// 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)?
|
||||
}
|
||||
Reference in New Issue
Block a user