Refactor entire codebase and rewrite documentation

This commit is contained in:
2025-10-10 18:06:34 +03:00
parent b4e9755c15
commit 8e471f2b9f
74 changed files with 3405 additions and 4149 deletions

View File

@@ -0,0 +1,144 @@
# Extending SQLite with Custom Functions
Build custom scalar and aggregate SQL functions that run inside SQLite.
DataLiteCore lets you register custom SQL functions that participate in expressions, queries, and
aggregations just like built-in ones. Each function is registered on a specific connection and
becomes available to all statements executed through that connection.
## Registering Functions
Use ``ConnectionProtocol/add(function:)`` to register a function type on a connection. Pass the
functions type, not an instance. DataLiteCore automatically manages function creation and
lifecycle — scalar functions are executed via their type, while aggregate functions are instantiated
per SQL invocation.
```swift
try connection.add(function: Function.Regexp.self) // Built-in helper
try connection.add(function: Slugify.self) // Custom scalar function
```
To remove a registered function, call ``ConnectionProtocol/remove(function:)``. This is useful for
dynamic plug-ins or test environments that require a clean registration state.
```swift
try connection.remove(function: Slugify.self)
```
## Implementing Scalar Functions
Subclass ``Function/Scalar`` to define a function that returns a single value for each call.
Override the static metadata properties — ``Function/name``, ``Function/argc``, and
``Function/options`` — to declare the functions signature, and implement its logic in
``Function/Scalar/invoke(args:)``. Return any type conforming to ``SQLiteRepresentable``.
```swift
final class Slugify: Function.Scalar {
override class var name: String { "slugify" }
override class var argc: Int32 { 1 }
override class var options: Function.Options { [.deterministic, .innocuous] }
override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
guard let value = args[0] as String?, !value.isEmpty else { return nil }
return value.lowercased()
.replacingOccurrences(of: "\\W+", with: "-", options: .regularExpression)
.trimmingCharacters(in: .init(charactersIn: "-"))
}
}
try connection.add(function: Slugify.self)
let rows = try connection.prepare(sql: "SELECT slugify(title) FROM articles")
```
## Implementing Aggregate Functions
Aggregate functions maintain internal state across multiple rows. Subclass ``Function/Aggregate``
and override ``Function/Aggregate/step(args:)`` to process each row and
``Function/Aggregate/finalize()`` to produce the final result.
```swift
final class Median: Function.Aggregate {
private var values: [Double] = []
override class var name: String { "median" }
override class var argc: Int32 { 1 }
override class var options: Function.Options { [.deterministic] }
override func step(args: any ArgumentsProtocol) throws {
if let value = args[0] as Double? {
values.append(value)
}
}
override func finalize() throws -> SQLiteRepresentable? {
guard !values.isEmpty else { return nil }
let sorted = values.sorted()
let mid = sorted.count / 2
return sorted.count.isMultiple(of: 2)
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid]
}
}
try connection.add(function: Median.self)
```
SQLite creates a new instance of an aggregate function for each aggregate expression in a query and
reuses it for all rows contributing to that result. Its safe to store mutable state in instance
properties.
## Handling Arguments and Results
Custom functions receive input through an ``ArgumentsProtocol`` instance. Use subscripts to access
arguments by index and automatically convert them to Swift types.
Two access forms are available:
- `subscript(index: Index) -> SQLiteValue` — returns the raw SQLite value without conversion.
- `subscript<T: SQLiteRepresentable>(index: Index) -> T?`— converts the value to a Swift type
conforming to ``SQLiteRepresentable``. Returns `nil` if the argument is `NULL` or cannot be
converted.
Use ``Function/Arguments/count`` to verify argument count before accessing elements. For
fine-grained decoding control, prefer the raw ``SQLiteValue`` form and handle conversion manually.
```swift
override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
guard args.count == 2 else {
throw SQLiteError(code: SQLITE_MISUSE, message: "expected two arguments")
}
guard let lhs = args[0] as Double?, let rhs = args[1] as Double? else {
return nil // returns SQL NULL if either argument is NULL
}
return lhs * rhs
}
```
Any type conforming to ``SQLiteRepresentable`` can be used both to read arguments and to return
results. Returning `nil` produces an SQL `NULL`.
## Choosing Function Options
Customize function characteristics via the ``Function/Options`` bitset:
- ``Function/Options/deterministic`` — identical arguments always yield the same result, enabling
SQLite to cache calls and optimize query plans.
- ``Function/Options/directonly`` — restricts usage to trusted contexts (for example, disallows
calls from triggers or CHECK constraints).
- ``Function/Options/innocuous`` — marks the function as side-effect-free and safe for untrusted
SQL.
Each scalar or aggregate subclass may return a different option set, depending on its behavior.
## Error Handling
Throwing from ``Function/Scalar/invoke(args:)``, ``Function/Aggregate/step(args:)``, or
``Function/Aggregate/finalize()`` propagates an error back to SQLite. DataLiteCore converts the
thrown error into a generic `SQLITE_ERROR` result code and uses its `localizedDescription` as the
message text.
You can use this mechanism to signal both validation failures and runtime exceptions during function
execution. Throwing an error stops evaluation immediately and returns control to SQLite.
- SeeAlso: ``Function``
- SeeAlso: [Application-Defined SQL Functions](https://sqlite.org/appfunc.html)