SqliteEngine 0.1 provides immutable, bounded Luau procedures for small application commands. A physical SQLite connection owns one warm Luau VM, compiled-function cache, and bounded prepared-statement cache. VMs are never shared between connections.
Luau is required native build input, not stock Lua. Run
bin/setup_native_luau.shonce before a source build. The pinned ref and commit are innative/locks/luau.lock.
Quick start
alias SqliteEngine.Procedures
{:ok, conn} = SqliteEngine.start_link(database: "commands.db")
source = ~S"""
local command, context = ...
local previous = db.scalar(
"SELECT value FROM counters WHERE id = ?",
command.counter_id
)
if previous == nil then
error("counter not found")
end
db.exec(
"UPDATE counters SET value = ? WHERE id = ?",
previous + command.increment,
command.counter_id
)
return {
command_id = context.command_id,
previous = previous,
current = previous + command.increment,
}
"""
{:ok, procedure} =
Procedures.install(conn, source, metadata: %{"name" => "increment-counter"})
command = %{
command_id: "command-001",
procedure: procedure.key,
arguments: %{"counter_id" => 10, "increment" => 2},
application_time: 1_723_500_000
}
{:ok, [receipt]} =
Procedures.apply_batch(conn, [command],
transaction: :immediate,
results: :stored,
security: :application_writes,
deterministic: true
)
{:ok, result} = Procedures.fetch_result(conn, receipt.result_ref)Create application tables before applying the example. Framework tables use the
private __sqlite_engine_ prefix and are initialized transactionally and idempotently.
Do not depend on their columns; public APIs are the compatibility contract.
Identity and catalog
A procedure key is exactly SHA-256(source bytes). Source is authoritative and
immutable. The catalog stores source, size, pinned Luau identity, runtime ABI,
compile options, bounded metadata, and an optional caller-supplied installation
time. Bytecode is not persisted.
On a connection cache miss SqliteEngine reads one catalog row, verifies all metadata
and the digest, compiles it once, and caches the loaded function. A hot call does
not read source or compile. Use Procedures.warm/3 to warm selected keys on a
specific physical connection; SqliteEngine never scans and compiles the whole
catalog.
info/3, bounded list/2, exists?/2, and delete/3 manage catalog entries.
Deleting a key does not unload it from an already-warm connection. Deletion is
rejected while durable receipts reference the key.
Canonical values
Arguments, contexts, SQLite cells, immediate results, durable payloads, and command hashes use canonical codec v1:
| Elixir | Luau / SQLite meaning |
|---|---|
nil | null / nil |
| boolean | boolean |
| signed 64-bit integer | exact i64; common arithmetic-range integers act as Luau numbers |
| finite float | Luau number / SQLite float |
| valid UTF-8 binary | text / string |
{:blob, binary} | BLOB / Luau buffer |
| list | dense table |
| map with UTF-8 binary keys | string-key table |
Maps encode in unsigned UTF-8 key-byte order. The codec rejects unsupported terms, invalid UTF-8 text, non-finite floats, integer overflow, improper lists, non-string map keys, excessive depth/count/bytes, malformed input, sparse or mixed result tables, cycles, and multiple return values. Empty input containers carry an internal kind marker so empty maps and lists round-trip.
Small exact integers use ordinary Luau arithmetic. Values outside IEEE-754's
exact integer range remain Luau i64 values and round-trip exactly; use Luau's
int library for arithmetic on those large i64 values. Integral Luau numeric
results in the exact range canonicalize as integers; use a non-integral value
when float identity is semantically required.
Each invocation receives fresh command data. Context tables are recursively
read-only and contain command_id, procedure_key, batch_id, and zero-based
index, in addition to caller context. IDs, timestamps, random outcomes, and
other reducer inputs must be supplied by the application.
Database capability
Procedures have one immutable db table:
db.exec(sql, ...)
db.scalar(sql, ...)
db.one(sql, ...)
db.all(sql, ...)
db.changes()
db.last_insert_rowid()execaccepts exactly one non-row-producing statement, exact parameters, and returnssqlite3_changes64().scalarrequires one column and zero or one row. No row is nil; a second row is an error.onereturns nil or one dense indexed row and rejects a second row.allreturns dense indexed rows and errors rather than truncating at a limit.- Binds and columns preserve NULL, i64, finite float, UTF-8 text, and BLOB.
- Containers are not SQL bind values.
SQL is cached by exact bytes and security profile. The per-connection LRU holds
128 entries, resets and clears every statement, and finalizes on eviction or
connection close. Use fixed parameterized SQL. Query ordering is SQLite's;
include ORDER BY when required.
Batches and transactions
apply_batch/3 validates every command and preflights every unique cold key
before BEGIN. It checks out one physical connection, begins once, performs one
native heterogeneous dispatch, stores receipts/results, and commits once.
Commands execute in input order. The first error stops execution; conversion,
execution, result, receipt, commit, or cancellation failure rolls the whole
batch back. Errors use zero-based batch_index. There is no continue-on-error
or per-command savepoint mode.
Transaction modes are :deferred, :immediate (default), and :exclusive.
Procedures cannot begin, commit, roll back, create savepoints, attach databases,
load extensions, mutate schema, or access private framework tables.
apply_batch_in_transaction/3 requires an already-active checked-out
connection. It neither commits nor rolls back. Warm keys before entering the
transaction, or explicitly pass allow_cold_load: true and accept the longer
lock duration.
Idempotency and durable results
A nonempty UTF-8 command ID is at most 255 bytes. Its content hash includes the
procedure key, canonical arguments, caller context, result mode, security
profile, and deterministic option. A committed identical ID/hash replays its
receipt without procedure execution. Reusing an ID with different content is a
structured :command_id_conflict. Duplicate IDs within a batch follow the same
rule.
Result modes:
:none: validate and discard the result; store a minimal receipt.:inline: return and retain up to 64 KiB for exact replay.:stored: atomically store up to 8 MiB and return a deterministic 32-byteResultRef; aggregate stored payload is at most 16 MiB per batch.
Effects, receipt, and result share the application transaction. Failed commands
have no receipt. Use lookup_receipt/3 to resolve an ambiguous commit response.
Use fetch_result/3, receipt-safe delete_result/3, and bounded
prune_results/2. Pruning requires an application cutoff and removes matching
results and receipts; there is no host clock or background cleanup service.
Limits, cancellation, and authority
%SqliteEngine.Procedures.Limits{} defaults to:
- 100 ms wall deadline and 1,000,000 Luau fuel checkpoints;
- 8 MiB invocation memory growth;
- 1,000 SQL statements and 10,000 rows;
- 8 MiB each SQL input and row output;
- 1 MiB arguments/results, depth 32, and 10,000 values;
- 1,000 commands and a 16 MiB aggregate batch envelope.
Options may narrow defaults or select values up to documented native hard
maxima; they cannot exceed maxima. Errors distinguish instruction, timeout,
cancellation, memory, statement, row, byte, result, argument, and security
failures. SQLite progress/cancel handling covers SQLite work; Luau interrupt
hooks cover pure loops. A failure rolls back the owning API and the next call
can reuse the connection. Procedures.stats/2 reports VM memory, execution,
compile, and statement-cache counters.
Security profiles are:
:application_writes(default): ordinary reads/DML, no transaction topology, schema, attach/detach, pragma, native extension, or private-table access;:read_only: additionally denies inserts, updates, and deletes;:trusted: broad ordinary SQL but still denies host transaction control, schema/topology escape, native extension loading, and hard-limit bypass.
deterministic: true denies SQLite clock/random functions. The VM never exposes
Luau OS time or math random facilities in any profile. Determinism still depends
on SQLite/Luau versions, explicit ordering, collation, floating point, and any
application-installed functions.
Pools, telemetry, testing
Every high-level pool operation is one DBConnection operation, so checkout,
preflight, transaction, native execution, result storage, and commit use one
physical connection. Reconnect loses only native caches; source remains in the
SQLite catalog. Pool queue timeout (:timeout) is separate from procedure
limits.timeout_ms.
Telemetry events are:
[:sqlite_engine, :procedures, :operation, :start]
[:sqlite_engine, :procedures, :operation, :stop]Metadata includes operation/status and structured error phase/reason. It never
contains source, SQL values, arguments, results, or full IDs/keys. Native work
and memory counters are available through stats/2 rather than payload-bearing
telemetry.
SqliteEngine.Procedures.Testing opens an initialized in-memory connection and
installs/warms named test procedures without adding a test-framework dependency.
For a multi-connection pool, use a file or shared-memory SQLite URI; ordinary
:memory: creates one independent database per physical connection.
Errors and low-level API
Public failures are %SqliteEngine.ProcedureError{}. Match phase, reason, and
batch_index; diagnostic text is not stable. Inspection redacts IDs and keys.
Rollback failure is retained separately. Busy/checkout errors are only marked
retryable when semantics prove it; callers should normally retry by command ID.
SqliteEngine.Sqlite3.prepare_luau/2, parameterless execute_luau/2, argument-aware
execute_luau/4, homogeneous execute_luau_batch/3, and native stats remain
available as low-level controls. Low-level execution requires an active
transaction and leaves rollback policy to its caller.
Build, benchmark, and limitations
Run:
bin/setup_native_luau.sh
bin/qa_check.sh
bin/benchmark_luau_roadmap.sh epic007
bin/compare_luau_roadmap.sh BASELINE.csv CANDIDATE.csv comparison.md
ROADMAP001 artifacts are under benchmark/results/roadmap001/. Raw samples are
immutable and record work dimensions; timing is evidence, never a correctness
test.
This feature is for bounded application procedures, not arbitrary hostile-code certification or OS process isolation. Deferred features include persistent bytecode, mutable aliases, packages/modules, background warming, queues, workflows, event outboxes, streaming cursors, custom codecs, signing/ACLs, per-command savepoints, and cross-database transactions. Precompiled support is limited to current OTP 29 / NIF 2.18 macOS and Linux x86_64/aarch64 artifacts for glibc and musl (Alpine). Windows, Android, and broad historical runtime matrices are not supported.