Changelog
View SourceAll notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.7.0 - 2026-06-12
Breaking
- Fan-out hooks renamed and made multi-subscriber. Every hook that
fans out events to a subscriber pid (update, wal, commit, rollback,
log) now uses the
register_X_hook/unregister_X_hook(handle)verbs and returns an opaque integer handle. Multiple subscribers can coexist independently on the same connection (or globally forlog_hook); each registration is independent. Migrations:set_update_hook(conn, pid)(returned:ok) →register_update_hook(conn, pid)(returns{:ok, handle})remove_update_hook(conn)→unregister_update_hook(conn, handle)(idempotent on unknown handles)- Same shape for:
wal_hook,commit_hook,rollback_hook,log_hook(the latter isregister_log_hook(pid)/unregister_log_hook(handle)since it's global). busy_handlerkeeps theset_busy_handler/remove_busy_handlerverbs because its callback returns a policy decision and multi-subscriber composition has no clean rule. A futureregister_busy_observer/1will offer fan-out observation alongside the single policy slot (seeproject_busy_handler_observer_splitdesign notes).
- Cancellable NIFs now take a list of tokens instead of a single
token.
XqliteNIF.query_cancellable/4,query_with_changes_cancellable/4,execute_cancellable/4,execute_batch_cancellable/3, andbackup_with_progress/6now expect the trailing argument to be[reference()](possibly empty) rather thanreference(). OR-semantics: any signalled token cancels the operation. Single-token callers wrap as[token]. The newXqlite.query_cancellable/4(and friends) plusXqlite.backup_with_progress/6accept either a single token or a list and normalise viaList.wrap/1. XqliteNIFis now the raw NIF boundary only. Every function inXqliteNIFis a bare NIF stub; all ergonomic wrappers moved to the user-facingXqlitemodule. Migrations:XqliteNIF.open_in_memory/0→Xqlite.open_in_memory/0(orXqliteNIF.open_in_memory(":memory:")to stay at the NIF layer)XqliteNIF.open_in_memory_readonly/0→Xqlite.open_in_memory_readonly/0XqliteNIF.serialize/1→Xqlite.serialize/1XqliteNIF.deserialize/2→Xqlite.deserialize/2XqliteNIF.load_extension/2→Xqlite.load_extension/2XqliteNIF.backup/2→Xqlite.backup/2XqliteNIF.restore/2→Xqlite.restore/2XqliteNIF.set_busy_handler/3(keyword-opts form) →Xqlite.set_busy_handler/3; the raw NIF stays asXqliteNIF.set_busy_handler/5
Added
- Opt-in
:telemetryinstrumentation across the whole API surface. Compile-time flag (config :xqlite, :telemetry_enabled, true+ recompile); when disabled (the default) no telemetry call exists in the bytecode at all. Span events (:start/:stopwith integer- nanosecondmonotonic_time/duration) for query / execute / execute_batch / explain_analyze and their cancellable variants, transactions and savepoints, streams (open / per-batch fetch / close), backup, wal_checkpoint, serialize / deserialize, extension loading, and pragma get/set. Cancellation lifecycle events:[:xqlite, :cancel, :token_created | :signalled | :honored]. Xqlite.Telemetry.bridge/2+bridge_log/1— forward the multi-subscriber hook fan-outs (update / wal / commit / rollback / progress, plus the global log hook) as[:xqlite, :hook, :*]telemetry events. New "Wiring xqlite telemetry" ExDoc guide covers conventions, the full event surface, and sample handlers.- Connection observability NIFs —
Xqlite.wal_checkpoint/3(:passive/:full/:restart/:truncate, returns structured busy / log-pages / checkpointed-pages),XqliteNIF.connection_stats/1,XqliteNIF.autocommit/1, andXqliteNIF.txn_state/2. Xqlite.busy_timeout/2— sets a plainsqlite3_busy_timeoutwhile cleanly reclaiming any xqlite-installed busy handler first. Prefer this overPRAGMA busy_timeout, which silently replaces the busy handler at the SQLite C level and stops{:xqlite_busy, …}delivery without removing our internal slot.- Busy-handler PRAGMA-replacement warning front-and-center in the module docs and README.
- WAL hook:
XqliteNIF.register_wal_hook/2+unregister_wal_hook/2. Sends{:xqlite_wal, db_name, pages}to each subscriber after each commit in WAL mode. Coexists with automatic checkpointing (see the slot-conflict fix below); only raw-SQLPRAGMA wal_autocheckpointstill steals the hook slot. - Commit hook:
XqliteNIF.register_commit_hook/2+unregister_commit_hook/2. Sends{:xqlite_commit}to each subscriber immediately before each commit. Observation-only — never vetoes the commit. - Rollback hook:
XqliteNIF.register_rollback_hook/2+unregister_rollback_hook/2. Sends{:xqlite_rollback}to each subscriber after each rollback. - Progress hook (multi-subscriber):
XqliteNIF.register_progress_hook/4+XqliteNIF.unregister_progress_hook/2plusXqlite.register_progress_hook/3/Xqlite.unregister_progress_hook/2. Multiple processes can subscribe independently to the same connection; each receives{:xqlite_progress, count, elapsed_ms}(or{:xqlite_progress, tag, count, elapsed_ms}if a tag is supplied), decimated by the per-subscriberevery_nknob. Coexists with cancellation on the single SQLite progress-handler slot — cancel signals interrupt before tick emission. - Multi-token cancellation: cancellable NIFs and
backup_with_progressaccept a list of tokens; any signal cancels (OR-semantics). The high-levelXqlite.query_cancellable/4family accepts either a single token or a list.
Fixed
- WAL hook ↔
wal_autocheckpointslot conflict. SQLite implements automatic checkpointing as a wal_hook, so the two share one C-level slot and silently disable each other. Both directions affected the in-development hook work:Xqlite.open/2's defaultwal_autocheckpointpragma evicted the master WAL callback (no subscriber ever received events), and on rawXqliteNIF.openconnections the master callback itself disabled autocheckpointing (unbounded WAL growth). The master callback now owns the slot and emulates the autocheckpoint — a passive checkpoint once the WAL reaches the configured threshold (default 1000 pages, mirroring SQLite) — and theset_pragmaNIF re-installs the master callback and syncs the threshold wheneverwal_autocheckpointis set. Remaining caveat (documented): issuingPRAGMA wal_autocheckpointthrough raw SQL (query,execute,execute_batch) bypasses the repair and still steals the slot.
Internal
- New
progress_dispatchRust module multiplexes the single SQLitesqlite3_progress_handlerslot between cancellation checkers (per cancellable-query lifetime) and tick subscribers (per-conn lifetime), via twoHookList<T>s. The C callback is registered eagerly at connection open and stays for the lifetime of the connection; subscriber install/uninstall is lock-free atomic-swap-and-reclaim. - New
HookList<T>primitive inhook_util: lock-free copy-on-write list of subscribers. Reads (in callbacks) are wait-free atomic loads; writes (under the conn Mutex) clone the Vec, mutate the clone, and atomic-swap. Vec is the proof-of-concept choice; ring buffer / lock- free structures are tracked as a benchmark-gated future optimisation. cancel.rs::ProgressHandlerGuardno longer touches FFI — it pushes oneCancelSubscriberper token onto the dispatch and unregisters them on drop. Holds the owningArc<AtomicBool>for each subscriber so the raw pointer stays valid for the registration's lifetime.- Shared
hook_utilRust module deduplicates term-construction (make_atom/make_binary) and atomic-slot lifecycle (install_hook/uninstall_hook/drop_hook) across the FFI-based hooks (busy_handler, wal_hook) and the rusqlite-closure hooks (update_hook, commit_hook, rollback_hook).
0.6.0 - 2026-04-19
Breaking
- Constraint errors are now structured.
:cannot_fetch_rowhas been removed as an outcome; constraint-violating statements now raise{:constraint_violation, subtype, details}withsubtypeas one of 13 typed atoms (:constraint_unique,:constraint_foreign_key,:constraint_check,:constraint_not_null,:constraint_primary_key,:constraint_trigger,:constraint_commit_hook,:constraint_function,:constraint_rowid,:constraint_pinned,:constraint_datatype,:constraint_vtab, and the generic:constraint_violationfallback) anddetailscarrying structuredtable,columns,index_name,constraint_namefields where applicable. Regex matching on error message strings is no longer needed. Callers catching{:error, {:cannot_fetch_row, _}}must update to match the new structured form.
Added
Xqlite.explain_analyze/3— structured execution report combiningEXPLAIN QUERY PLAN, per-scan runtime counters fromsqlite3_stmt_scanstatus_v2(loops, rows visited, estimated rows, name, parent, selectid), statement-level counters fromsqlite3_stmt_status(vm_step, sort, fullscan_step, memused, etc.), and wall-clock execution time. SQLite's closest analog to PostgreSQL'sEXPLAIN (ANALYZE).Xqlite.open/2andXqlite.open_in_memory/1— high-level open functions with validated options. Options are type-checked at the boundary and produce structured errors on misuse.Xqlite.enable_strict_table/2— converts an existing table to STRICT mode via the canonical SQLite rewrite dance.Xqlite.check_strict_violations/2— pre-scans a table for rows that would fail STRICT-mode type enforcement, so callers can fix data before flipping the switch.- Structured STRICT datatype violations. When a STRICT table
rejects a write, the error carries
source_typeandtarget_typeatoms (:integer,:real,:text,:blob,:null) so callers can reason about the mismatch without parsing messages. - Structured invalid-option errors from the option-validation layer; no regex on error text.
0.5.2 - 2026-03-16
Added
XqliteNIF.query_with_changes/3andquery_with_changes_cancellable/4— return rows plus thesqlite3_changes()count in one atomic call, captured inside the connection Mutex so the count cannot be stolen by an intervening statement. Zero for non-DML results (detected by empty column list).Xqlite.query/3high-level wrapper that returns an%Xqlite.Result{}with a populatedchangesfield.Xqlite.Resultgained achangesfield.
0.5.1 - 2026-03-16
Added
XqliteNIF.changes/1— returns the row count affected by the most recent DML (wrapssqlite3_changes()).XqliteNIF.total_changes/1— cumulative row count across the connection's lifetime (wrapssqlite3_total_changes()).
0.5.0 - 2026-03-16
Major feature release. Substantial surface added; several subtle behavioral changes worth noting on upgrade.
Added
- Online backup API.
XqliteNIF.backup/2+restore/2(one-shot), plusbackup_with_progress/6(page-by-page with progress messages to a PID, cancel-token support). - Session extension.
session_new,session_attach,session_changeset,session_delete,changeset_invert,changeset_concat,changeset_applywith conflict strategies (:omit,:replace,:abort). - Incremental blob I/O.
blob_open,blob_read,blob_write,blob_close. Read and write multi-GB column values without loading them into memory. - Extension loading.
enable_load_extension/2andload_extension/2,3. Opt-in; disabled by default. - Serialize / deserialize.
serialize/1captures the entire live database as a single binary byte-for-byte identical to its on-disk form;deserialize/2loads it back. - Log hook and update hook via raw
enif_send. Per-connection update notifications as{:xqlite_update, action, db, table, rowid}; global log hook as{:xqlite_log, code, message}. - Type extension behaviour.
Xqlite.TypeExtensionfor bidirectional Elixir↔SQLite conversion. Built-ins shipped forDateTime,Date,Time,NaiveDateTime. Xqlite.Resultstruct implementing theTable.Readerprotocol — consumable directly by Explorer, Kino, VegaLite.XqliteNIF.transaction_status/1— structured query of the current connection's transaction state.- Read-only opens.
open_readonly/1andopen_in_memory_readonly/1. - Transaction modes.
deferred,immediate,exclusive. - Schema-prefixed PRAGMAs.
:db_nameoption for PRAGMAs that accept a database name parameter.
Changed
- PRAGMA schema reworked from a keyword list to
Xqlite.PragmaSpecstructs. Public shape change for anyone introspecting PRAGMA metadata. - PRAGMA SET now returns the echoed value instead of discarding it,
matching the
{:ok, echoed_value}shape of the rest of the API. XqliteNIF.close/1eagerly releases the underlying SQLite connection rather than waiting for Elixir GC.- rusqlite upgraded 0.38 → 0.39. UTF-8 errors now carry the column index of the offending value.
Fixed
- Stream finalization data race where
sqlite3_finalizecould run without the connection Mutex held — a BEAM-segfault-class bug. stream_fetchnow holds the Mutex for the entire fetch loop (was dropping it between steps).- TOCTOU race in the
with_connclosed-flag check. - Atom-table exhaustion protection: user input no longer becomes atoms unconditionally.
- SQL length overflow guard in
stream_open. - Integer-truncation guard for FFI bind calls.
- Identifier quoting: single quotes → double quotes for SQLite spec compliance.
- PRAGMA name validation against SQL injection (reject non-identifier PRAGMA names).
- PRAGMA validation catch-all for unknown names and corrected numeric ranges.
- Interruption detection, cancel ordering, and error-code mapping.
0.4.1 - 2026-03-13
Fixed
- Documentation, README, CI badge, and stale version references reconciled across the project.
0.4.0 - 2026-03-13
Promotes v0.4.0-rc.1 to stable. No additional changes since rc.1.
0.4.0-rc.1 - 2026-03-13
Added
- Precompiled NIFs via
rustler_precompiled. No Rust toolchain is required to install from Hex. 8 targets covered:aarch64-apple-darwin,x86_64-apple-darwin,aarch64-unknown-linux-gnu,x86_64-unknown-linux-gnu,aarch64-unknown-linux-musl,x86_64-unknown-linux-musl,riscv64gc-unknown-linux-gnu,x86_64-pc-windows-msvc.
Changed
- Rust edition upgraded 2018 → 2024.
0.3.1 - 2025-12-06
Changed
- Dependencies refreshed.
0.3.0 - 2025-11-24
Initial public release. The supported SQLite functionality:
- Bundled SQLite — no system install required.
- Queries, execution, and parameter binding (positional and named).
- Transactions with named savepoints (nested-transaction support).
- Streaming row iteration compatible with
Stream.resource/3. - Per-operation cancellation. Progress-handler-based; any process can cancel an in-progress operation without holding the connection handle.
- Typed PRAGMA system with validated get/set.
- Schema introspection via
PRAGMA table_xinfo,index_list,index_xinfo,foreign_key_list, etc. — surfaced as structured data, including generated and hidden columns. - STRICT table support.
- Read-only database opens.
- Structured error surface — constraint violations and failure categories mapped to typed atoms (no string parsing needed by callers).
- SQLite introspection —
compile_optionsandsqlite_version.