7.9.33. SQL-32 — User-defined SQL scalar functions
SQL is fluent at filtering and grouping but speaks a small built-in
vocabulary. When an expression doesn’t fit (a custom hash, a
domain-specific scoring formula, a string normalizer),
register_function binds an ordinary daslang function as a SQL
scalar so it can be used inside any expression — WHERE,
projection, ORDER BY, indexes, view bodies, anywhere a built-in
scalar would go.
7.9.33.1. The contract
db |> register_function(name : string, fn : @@<...>;
deterministic : bool = false;
directonly : bool = false)
name— the SQL identifier the function will be invoked under. Re-registering the same name replaces the prior binding.fn— a daslang function pointer (@@form). The macro inspects the pointer’s signature at compile time, derives the per-arg + return type tags, and ships them to the C++ trampoline.deterministic = true— same inputs always yield the same output. Required to use the fn insideCREATE INDEX ... ON tbl(myfn(col))or in a generated column.directonly = true— the fn cannot be called from triggers, views, orCHECK(...)constraints. Useful to lock down side-effecting helpers that should run only from application SQL.
Lifetime. Registration is per-connection. The function is available
until the connection closes (or until DROP FUNCTION if exposed in
a future chunk).
NULL handling. If any argument is SQL NULL, the trampoline
emits NULL without invoking the daslang function — the standard
SQL behavior of built-in scalars (abs(NULL) -> NULL,
length(NULL) -> NULL). Explicit Option<T> arg types for
in-function NULL handling are planned but not yet shipped.
Panic recovery. A panic inside the daslang function is caught and
surfaced to SQLite as an error on that statement; try_query
returns Err carrying the panic message. The connection itself is
unaffected and the next statement runs normally.
7.9.33.2. Supported types
register_function maps daslang scalar types to SQLite cells:
daslang type |
SQLite cell |
|---|---|
|
INTEGER |
|
REAL |
|
TEXT |
Unsupported types (structs, arrays, pointers, lambdas, classes) fail at compile time with a per-position diagnostic. Up to 4 arguments are supported in v1.
7.9.33.3. End-to-end
A scoring formula, a string hasher, a 2-D distance: three real-shaped
SQL helpers, all marked deterministic so SQLite is allowed to factor
them out of inner loops or use them as the indexed expression in a
CREATE INDEX.
require sqlite/sqlite_boost
require sqlite/sqlite_linq
require math
def damage_formula(base : float; armor : int; mult : float) : float {
return base * (1.0f - float(armor) / 100.0f) * mult
}
def short_hash(s : string) : int64 {
var h = 1469598103934665603l
for (c in s) {
h = (h ^ int64(c)) * 1099511628211l
}
return h
}
def euclid(x1 : float; y1 : float; x2 : float; y2 : float) : float {
let dx = x2 - x1
let dy = y2 - y1
return sqrt(dx * dx + dy * dy)
}
[sql_table(name = "Enemies")]
struct Enemy {
@sql_primary_key Id : int
Name : string
Hp : float
Armor : int
}
[export]
def main {
with_sqlite(":memory:") $(db) {
db |> create_table(type<Enemy>)
// ... insert rows ...
db |> register_function("damage_formula", @@damage_formula, true)
db |> register_function("short_hash", @@short_hash, true)
db |> register_function("euclid", @@euclid, true)
}
}
Once registered, the function is just another SQL scalar:
Inside a projection (per-row SELECT result) —
let dmg <- (db |> try_run_select(
"SELECT damage_formula(100.0, Armor, 1.5) FROM Enemies ORDER BY Id",
$(var stmt : sqlite3_stmt?) {},
$(stmt : sqlite3_stmt?) : float {
return float(sqlite3_column_double(stmt, 0))
})) |> unwrap
Inside a WHERE clause (filter predicate) —
let weak_count : int = db |> query_scalar(
"SELECT COUNT(*) FROM Enemies WHERE damage_formula(100.0, Armor, 1.5) > 50",
type<int>)
Inside ORDER BY (sort key) —
let first_by_hash = db |> query_scalar(
"SELECT Name FROM Enemies ORDER BY short_hash(Name) LIMIT 1",
type<string>)
Inside an aggregate (multi-arg numeric fn) —
let total = db |> query_scalar(
"SELECT SUM(euclid(StartX, StartY, EndX, EndY)) FROM Trips",
type<double>)
7.9.33.4. Provider-specific
register_function is SQLite-provider-specific. Other backends
have their own user-defined-function registration rails (PostgreSQL’s
CREATE FUNCTION with PL/pgSQL, MySQL’s UDF interface, MSSQL’s
CREATE FUNCTION + assemblies). A future cross-provider abstraction
would have to round-trip across all of them; for now, the daslang
function lives behind sqlite/sqlite_boost.
7.9.33.5. Deferred
Aggregate functions (
sqlite3_create_functionwith step + finalize callbacks). Useful for customGROUP BYaggregates —MEDIAN,STDEV, percentile families. Defer until a real consumer asks.Window functions (
sqlite3_create_window_function). Specialized enough to wait for a real use case.Explicit
Option<T>arg types. Today the trampoline short-circuitsNULLarguments; an explicitOption<T>arg would let the function decide whatNULLmeans. Planned.Variadic + > 4 args. v1 caps at 4 positional args. Lift the cap when something real wants it.
See also
Full source: tutorials/sql/32-sql_functions.das
Previous tutorial: SQL-31 — Views
Next tutorial: SQL-33 — PRAGMA tuning