Xqlite (Xqlite v0.12.1)

View Source

This is the central module of this library. All SQLite operations can be performed from here. Note that they delegate to other modules which you can also use directly.

Summary

Types

Every reason an xqlite call can fail with.

Controls how stream/4 reacts to a mid-fetch error; see its :on_error option for the per-mode element shapes.

Functions

Returns {:ok, true} when the connection is in auto-commit mode (no active transaction), {:ok, false} otherwise.

Backs up a schema to a file.

Online backup with progress messages and cancellation. Accepts either a single cancel token or a list (OR-semantics).

Begins a transaction in the given mode (:deferred, :immediate, or :exclusive). Emits [:xqlite, :transaction, :begin] telemetry.

Binds parameters to a prepared statement.

Sets how long the connection waits on a locked database, going through the xqlite busy slot.

Signals a cancellation token. Emits [:xqlite, :cancel, :signalled].

Returns the number of rows changed by the most recently completed INSERT, UPDATE, or DELETE on this connection.

Checks an existing table for everything that would stop it becoming STRICT.

Clears all parameter bindings on a prepared statement back to NULL.

Closes the connection, releasing the underlying SQLite handle.

Returns the result column names of a prepared statement.

Commits the current transaction. Emits [:xqlite, :transaction, :commit].

Returns the compile-time options the linked SQLite library was built with, as a list of strings (PRAGMA compile_options).

Returns a snapshot of the connection's sqlite3_db_status counters (lookaside, pager cache, schema and statement memory, cache hit/miss/spill, deferred foreign keys).

Creates a cancellation token. Emits [:xqlite, :cancel, :token_created].

Returns the filesystem path of the connection's main database.

Deserializes a binary into a database, replacing its current contents.

Disables foreign key constraint enforcement for the given database connection (default behavior).

Enables foreign key constraint enforcement for the given database connection.

Enables or disables extension loading on the connection.

Converts an existing table to STRICT mode via table rebuild.

Executes a non-returning SQL statement and returns a %Xqlite.Result{}.

Executes a SQL batch (multiple statements separated by semicolons).

Cancellable execute_batch/2. Accepts either a single cancel token or a list.

Cancellable execute/3. Accepts either a single cancel token or a list.

Runs a SQL statement and returns an %Xqlite.ExplainAnalyze{} report.

Finalizes a prepared statement, releasing its SQLite resources.

Returns the CREATE ... SQL for a schema object as recorded in sqlite_schema, or {:ok, nil} when no object with that name exists.

Reads a PRAGMA value from the connection.

Returns the rowid of the most recent successful INSERT on this connection.

Loads a SQLite extension from the shared library at path.

Advances a prepared statement up to batch_size rows.

Opens a database connection with opinionated defaults and validated options.

Opens an in-memory database with opinionated defaults and validated options.

Opens a read-only connection to an in-memory SQLite database.

Opens a read-only connection to an existing database file.

Opens a connection to a private temporary on-disk database.

Prepares a manually managed statement.

Executes a SQL query and returns a %Xqlite.Result{} struct.

Cancellable query/3. Accepts either a single cancel token or a list of tokens; OR-semantics — any signalled token interrupts the query.

Cancellable query_with_changes/3. Accepts either a single cancel token or a list.

Registers a busy-contention observer on the connection.

Registers a progress-tick subscriber on the connection.

Releases a savepoint. Emits [:xqlite, :savepoint, :release].

Removes any authorizer installed on the connection.

Removes the busy retry policy from the connection.

Resets a prepared statement so it can be stepped from the start again.

Restores a schema from a file.

Rolls back the current transaction. Emits [:xqlite, :transaction, :rollback] with reason: :user_initiated.

Rolls back to a savepoint without releasing it. Emits [:xqlite, :savepoint, :rollback_to].

Creates a savepoint with the given name. Emits [:xqlite, :savepoint, :create].

Returns column details for a table or view as Xqlite.Schema.ColumnInfo structs (PRAGMA table_xinfo), or {:ok, []} when the table does not exist.

Lists all databases attached to the connection as Xqlite.Schema.DatabaseInfo structs (PRAGMA database_list).

Returns the foreign keys defined on a table as Xqlite.Schema.ForeignKeyInfo structs (PRAGMA foreign_key_list).

Returns the columns of an index as Xqlite.Schema.IndexColumnInfo structs (PRAGMA index_xinfo), ordered by position in the index.

Returns all indexes on a table as Xqlite.Schema.IndexInfo structs (PRAGMA index_list), including those backing PRIMARY KEY and UNIQUE constraints.

Lists tables, views, and virtual tables as Xqlite.Schema.SchemaObjectInfo structs (PRAGMA table_list).

Serializes a database to a contiguous binary.

Installs a deny-list authorizer on the connection.

Sets the busy retry POLICY on the connection.

Sets a PRAGMA value on the connection.

Returns the version string of the linked SQLite C library.

Advances a prepared statement one row.

Creates a stream that executes a query and emits rows as string-keyed maps.

Returns the total number of rows changed by all INSERT, UPDATE, and DELETE statements since the connection was opened, including changes made by triggers. Wraps XqliteNIF.total_changes/1. No telemetry is emitted.

Returns whether the connection is currently inside a transaction.

Returns the transaction state of a schema: :none, :read, :write, or :unknown (a future SQLite state not mapped yet).

Unregisters a busy-contention observer by handle.

Unregisters a progress-tick subscriber by handle.

Performs a WAL checkpoint on the connection.

Types

conn()

@type conn() :: reference()

constraint_details()

@type constraint_details() :: %{
  message: String.t(),
  table: String.t() | nil,
  columns: [String.t()],
  index_name: String.t() | nil,
  constraint_name: String.t() | nil,
  source_type: storage_class(),
  target_type: storage_class()
}

constraint_kind()

@type constraint_kind() ::
  :constraint_check
  | :constraint_commit_hook
  | :constraint_datatype
  | :constraint_foreign_key
  | :constraint_function
  | :constraint_not_null
  | :constraint_pinned
  | :constraint_primary_key
  | :constraint_rowid
  | :constraint_trigger
  | :constraint_unique
  | :constraint_vtab
  | :constraint_violation

error()

@type error() :: {:error, error_reason()}

error_reason()

@type error_reason() ::
  :connection_closed
  | :execute_returned_results
  | :extension_loading_disabled
  | :invalid_conflict_strategy
  | :invalid_transaction_mode
  | :multiple_statements
  | :null_byte_in_string
  | :operation_cancelled
  | :statement_finalized
  | :transaction_in_progress
  | {:authorization_denied, integer(), String.t()}
  | {:cannot_convert_atom_to_string, String.t()}
  | {:cannot_convert_to_sqlite_value, String.t(), String.t()}
  | {:cannot_execute, String.t()}
  | {:cannot_execute_pragma, String.t(), String.t()}
  | {:cannot_open_database, String.t(), integer(), String.t()}
  | {:constraint_violation, constraint_kind(), constraint_details()}
  | {:database_busy_or_locked, integer(), String.t()}
  | {:expected_keyword_list, String.t()}
  | {:expected_keyword_tuple, String.t()}
  | {:expected_list, String.t()}
  | {:from_sql_conversion_failure, non_neg_integer(), atom(), String.t()}
  | {:index_exists, String.t()}
  | {:integral_value_out_of_range, non_neg_integer(), integer()}
  | {:internal_encoding_error, String.t()}
  | {:invalid_authorizer_action, atom()}
  | {:invalid_batch_size, %{provided: term(), minimum: 1}}
  | {:invalid_cancel_tokens, term()}
  | {:invalid_column_index, non_neg_integer()}
  | {:invalid_column_name, String.t()}
  | {:invalid_column_type, non_neg_integer(), String.t(), atom()}
  | {:invalid_on_error, term()}
  | {:invalid_open_option,
     %{key: atom(), reason: :unknown_key, allowed: [atom()], value: nil}
     | %{
         key: atom(),
         reason: :invalid_value,
         value: term(),
         message: String.t()
       }}
  | {:invalid_pages_per_step, integer()}
  | {:invalid_parameter_count,
     %{provided: non_neg_integer(), expected: non_neg_integer()}}
  | {:invalid_parameter_name, String.t()}
  | {:invalid_pragma_name, String.t()}
  | {:invalid_pragma_value, %{pragma: atom(), value: term()}}
  | {:invalid_stream_handle, String.t()}
  | {:lock_error, String.t()}
  | {:no_such_index, String.t()}
  | {:no_such_table, String.t()}
  | {:not_a_plain_table,
     %{table: String.t(), type: Xqlite.Schema.Types.object_type()}}
  | {:read_only_database, integer(), String.t()}
  | {:rowid_shadowed, String.t()}
  | {:schema_changed, integer(), String.t()}
  | {:schema_parsing_error, String.t(), {:unexpected_value, String.t()}}
  | {:sql_input_error, sql_input_error()}
  | {:sqlite_failure, integer(), integer(), String.t() | nil}
  | {:table_exists, String.t()}
  | {:to_sql_conversion_failure, String.t()}
  | {:unknown_pragma, atom()}
  | {:unsupported_atom, String.t()}
  | {:unsupported_data_type, atom()}
  | {:utf8_error, non_neg_integer(), String.t()}
  | {:without_rowid_unsupported, String.t()}

Every reason an xqlite call can fail with.

Four of them name a database object: :no_such_table, :no_such_index, :table_exists and :index_exists carry the name SQLite itself printed, not the sentence around it. SQLite writes that name in two ways and the payload keeps whichever one it chose: the "no such" pair print the resolved name with its quotes stripped and keep a schema qualifier when the statement named one ("main.people"), while the "already exists" pair never carry a qualifier — :table_exists echoes the identifier exactly as the statement wrote it, quotes included, and :index_exists prints the resolved name. The quoting is SQLite's own, not this library's.

query_result()

@type query_result() :: %{
  columns: [String.t()],
  rows: [[sqlite_value()]],
  num_rows: non_neg_integer()
}

sql_input_error()

@type sql_input_error() :: %{
  code: integer(),
  message: String.t(),
  sql: String.t(),
  offset: integer()
}

sqlite_value()

@type sqlite_value() :: integer() | float() | binary() | nil

stmt()

@type stmt() :: reference()

storage_class()

@type storage_class() :: :integer | :real | :text | :blob | nil

stream_on_error()

@type stream_on_error() :: :raise | :halt | :emit_error

Controls how stream/4 reacts to a mid-fetch error; see its :on_error option for the per-mode element shapes.

Functions

autocommit(conn)

@spec autocommit(conn()) :: {:ok, boolean()} | error()

Returns {:ok, true} when the connection is in auto-commit mode (no active transaction), {:ok, false} otherwise.

The inverse view of transaction_status/1, matching SQLite's sqlite3_get_autocommit. Wraps XqliteNIF.autocommit/1. No telemetry is emitted.

backup(conn, dest_path, schema \\ "main")

@spec backup(conn(), String.t(), String.t()) :: :ok | error()

Backs up a schema to a file.

Copies the named schema (default "main") to the file at dest_path. The destination is created or overwritten. The source remains readable during the backup.

backup_with_progress(conn, schema, dest_path, pid, pages_per_step, token_or_tokens)

@spec backup_with_progress(
  conn(),
  String.t(),
  String.t(),
  pid(),
  pos_integer(),
  reference() | [reference()]
) :: :ok | error()

Online backup with progress messages and cancellation. Accepts either a single cancel token or a list (OR-semantics).

Sends {:xqlite_backup_progress, remaining, pagecount} to pid after each pages_per_step-page step. Returns {:error, :operation_cancelled} if any token signals between steps.

pages_per_step must be a positive integer. A non-positive value returns {:error, {:invalid_pages_per_step, value}} — passing 0 would otherwise make SQLite copy no pages while reporting "more", spinning forever.

begin(conn, mode \\ :deferred)

@spec begin(conn(), term()) :: :ok | error()

Begins a transaction in the given mode (:deferred, :immediate, or :exclusive). Emits [:xqlite, :transaction, :begin] telemetry.

Any other term in the mode position returns {:error, :invalid_transaction_mode} and starts nothing.

bind(stmt, params)

@spec bind(stmt(), list()) :: :ok | error()

Binds parameters to a prepared statement.

Accepts a plain list for positional placeholders (?1, ?2, …; the count must match, otherwise {:error, {:invalid_parameter_count, %{provided: _, expected: _}}}) or a keyword list for named placeholders. Once stepping has started, call reset/1 before rebinding — SQLite rejects mid-run rebinds.

busy_timeout(conn, ms)

@spec busy_timeout(conn(), non_neg_integer()) :: :ok | error()

Sets how long the connection waits on a locked database, going through the xqlite busy slot.

Removes the busy retry policy first (as remove_busy_policy/1 does), then applies ms:

  • With busy observers registered, the slot stays theirs and the timeout applies through it: observers keep receiving {:xqlite_busy, …} messages, the connection waits up to ms on SQLite's own retry schedule, and unregistering the last observer keeps this timeout. While the slot is held, PRAGMA busy_timeout reads 0 — SQLite zeroes it whenever a callback is installed — even though the wait still applies.
  • With no observers, SQLite's own timeout handler takes the slot and PRAGMA busy_timeout reads ms back.

ms is the timeout in milliseconds. 0 disables the timeout entirely (SQLite returns SQLITE_BUSY immediately on contention). SQLite stores the timeout as a 32-bit integer, so values above 2_147_483_647 (about 24.8 days) are refused with {:error, {:cannot_execute, reason}} rather than silently clamped.

Warning — a raw PRAGMA busy_timeout still steals the slot

Running PRAGMA busy_timeout = N (or XqliteNIF.set_pragma(conn, "busy_timeout", ms)) as raw SQL replaces our C callback with SQLite's built-in one without going through the slot, and every registered observer silently stops receiving {:xqlite_busy, …} messages. This function is the safe path.

cancel_operation(token)

@spec cancel_operation(reference()) :: :ok | error()

Signals a cancellation token. Emits [:xqlite, :cancel, :signalled].

Idempotent at the SQLite level — signalling twice is the same as once. Telemetry fires on every call, so consumers see distinct signal events even from repeated signals.

There is no un-signal: once signalled, the token stays cancelled for its lifetime. A signalled token is spent — passing it to another cancellable operation cancels that operation immediately. Create a fresh token per operation; see the "Cancel tokens are single-use" section of the Gotchas guide.

changes(conn)

@spec changes(conn()) :: {:ok, non_neg_integer()} | error()

Returns the number of rows changed by the most recently completed INSERT, UPDATE, or DELETE on this connection.

The counter is sticky: non-DML statements (SELECT, DDL, PRAGMA) leave it untouched, so it reports the last DML's count rather than resetting to 0. For an atomically captured count prefer query/4 or execute/4, whose Xqlite.Result.t/0 carries changes taken inside the connection lock. Wraps XqliteNIF.changes/1. No telemetry is emitted.

check_strict_violations(conn, table)

@spec check_strict_violations(conn(), String.t()) :: {:ok, [map()]} | error()

Checks an existing table for everything that would stop it becoming STRICT.

Returns {:ok, []} if the table is clean, or {:ok, violations}. A violation is one of three maps:

  • a stored value SQLite would refuse — %{rowid: _, column: _, actual_type: _, expected_type: _};
  • a column whose declared type is not one STRICT knows — %{kind: :unknown_declared_type, column: name, declared: type}. STRICT accepts only INT, INTEGER, REAL, TEXT, BLOB and ANY, in any case, so VARCHAR(255), DATETIME and NUMERIC are all refused;
  • a column with no declared type at all — %{kind: :missing_declared_type, column: name}.

An ANY column is checked by nothing: STRICT accepts the type and puts no rule on the values.

The name resolves the way SQLite resolves an unqualified name: the temp schema first, then main, then the attached databases in attach order.

Objects that are not plain tables — views, virtual tables and the shadow tables that hold a virtual table's storage — return {:error, {:not_a_plain_table, %{table: name, type: type}}}, where type is :view, :virtual or :shadow.

WITHOUT ROWID tables are not supported — the check reads each row's rowid, which such a table does not have — and return {:error, {:without_rowid_unsupported, table}}.

This is a read-only check — it does not modify the table.

clear_bindings(stmt)

@spec clear_bindings(stmt()) :: :ok | error()

Clears all parameter bindings on a prepared statement back to NULL.

close(conn)

@spec close(conn()) :: :ok

Closes the connection, releasing the underlying SQLite handle.

Idempotent: closing an already-closed connection returns :ok. Any operation on a closed connection returns {:error, :connection_closed}.

Closing finalizes any prepared statement, stream or incremental blob still open on the connection and then frees the SQLite handle. Those handles stay usable as terms: an operation on one returns {:error, :connection_closed}, and finalizing or closing one returns :ok. A session is not covered — delete sessions before closing (see XqliteNIF.session_delete/1).

Emits [:xqlite, :close, :start | :stop] telemetry.

column_names(stmt)

@spec column_names(stmt()) :: {:ok, [String.t()]} | error()

Returns the result column names of a prepared statement.

Live statements reflect SQLite's auto-reprepare after schema changes (e.g. SELECT * re-expansion); finalized statements answer with the prepare-time snapshot.

commit(conn)

@spec commit(conn()) :: :ok | error()

Commits the current transaction. Emits [:xqlite, :transaction, :commit].

compile_options(conn)

@spec compile_options(conn()) :: {:ok, [String.t()]} | error()

Returns the compile-time options the linked SQLite library was built with, as a list of strings (PRAGMA compile_options).

Useful to confirm features such as ENABLE_FTS5 are present. Wraps XqliteNIF.compile_options/1. No telemetry is emitted.

connection_stats(conn)

@spec connection_stats(conn()) :: {:ok, map()} | error()

Returns a snapshot of the connection's sqlite3_db_status counters (lookaside, pager cache, schema and statement memory, cache hit/miss/spill, deferred foreign keys).

See XqliteNIF.connection_stats/1 for the full key list. Call repeatedly for time-series monitoring. No telemetry is emitted.

create_cancel_token()

@spec create_cancel_token() :: {:ok, reference()} | error()

Creates a cancellation token. Emits [:xqlite, :cancel, :token_created].

The token is an opaque reference passed into cancellable operations (query_cancellable/4, execute_cancellable/4, etc.). Signalling it via cancel_operation/1 from any process interrupts in-flight cancellable operations holding the same token.

A token is single-use: its flag is set once and never reset, so once cancel_operation/1 has signalled it the token stays signalled. Reusing an already-signalled token cancels the next operation the moment it starts — create a fresh token per cancellable operation. See the "Cancel tokens are single-use" section of the Gotchas guide.

db_path(conn)

@spec db_path(conn()) :: {:ok, String.t() | nil} | error()

Returns the filesystem path of the connection's main database.

{:ok, path} for file-backed databases, {:ok, nil} for in-memory and temporary databases (they have no backing file). No telemetry is emitted.

deserialize(conn, data, schema \\ "main", read_only \\ false)

@spec deserialize(conn(), binary(), String.t(), boolean()) :: :ok | error()

Deserializes a binary into a database, replacing its current contents.

The binary must be a valid SQLite database image (as produced by serialize/2). After deserialization the connection operates on the new database entirely in memory.

schema identifies which attached database to replace (default "main"). read_only marks the deserialized image as read-only (default false).

disable_foreign_key_enforcement(conn)

@spec disable_foreign_key_enforcement(conn()) :: {:ok, term()} | error()

Disables foreign key constraint enforcement for the given database connection (default behavior).

See enable_foreign_key_enforcement/1 for details.

enable_foreign_key_enforcement(conn)

@spec enable_foreign_key_enforcement(conn()) :: {:ok, term()} | error()

Enables foreign key constraint enforcement for the given database connection.

By default, SQLite parses foreign key constraints but does not enforce them. This function turns on enforcement.

See: SQLite PRAGMA foreign_keys

enable_load_extension(conn, enabled \\ true)

@spec enable_load_extension(conn(), boolean()) :: :ok | error()

Enables or disables extension loading on the connection.

Defaults to true. Wraps XqliteNIF.enable_load_extension/2 and emits [:xqlite, :extension, :enable] telemetry.

Warning — enabling has no automatic undo

There is no automatic disable. Once enabled, the SQL-level load_extension() function stays callable for the rest of the connection's life — including from SQL you did not write — until you call this function with false yourself.

enable_strict_table(conn, table)

@spec enable_strict_table(conn(), String.t()) :: :ok | {:error, term()}

Converts an existing table to STRICT mode via table rebuild.

This creates a new STRICT table, copies all data, drops the original, and renames the new table — all inside a transaction.

If anything check_strict_violations/2 reports would stop the conversion — a stored value SQLite would refuse, a column whose declared type STRICT does not know, a column with no declared type — the operation fails with {:error, {:strict_violations, violations}} before any SQL runs, and the original table is left untouched.

Any name SQLite accepts works, whatever quoting the stored CREATE TABLE statement uses: the rebuild rewrites that statement's own name token and quotes every name it emits. A table that is already STRICT returns :ok and no statement runs at all.

The name resolves the way SQLite resolves an unqualified name — the temp schema first, then main, then the attached databases in attach order — and the rebuild runs in the schema the table was found in.

Objects that are not plain tables — views, virtual tables and the shadow tables that hold a virtual table's storage — return {:error, {:not_a_plain_table, %{table: name, type: type}}}.

WITHOUT ROWID tables are not supported and return {:error, {:without_rowid_unsupported, table}}.

The rebuild puts back everything SQLite hangs off a table: the rowids, gaps included; the indexes; the triggers, a TEMP trigger — which lives in the temp schema, not in the table's — included; and the views that read the table keep answering. The rows of child tables are left alone: PRAGMA foreign_keys is switched off around the rebuild, so dropping the original runs no ON DELETE action, and PRAGMA legacy_alter_table is switched on around the rename, so a view still naming the original does not fail it. Both pragmas are read first and put back afterwards, on every path. The copy's column list comes from PRAGMA table_info, so a table with generated columns converts too.

The rebuild needs a transaction of its own. Called while the caller has one open it returns {:error, :transaction_in_progress} and touches nothing — its rollback would discard the caller's uncommitted rows. Two more refusals come before any statement runs: an existing <table>_xqlite_strict_rebuild in the same schema returns {:error, {:table_exists, name}}, and a table declaring all three of rowid, _rowid_ and oid as columns without one of them being the INTEGER PRIMARY KEY alias returns {:error, {:rowid_shadowed, table}}, because with every spelling taken the copy cannot name the rowid.

Options

None currently.

Examples

:ok = Xqlite.enable_strict_table(conn, "users")

execute(conn, sql, params \\ [], opts \\ [])

@spec execute(conn(), String.t(), list() | keyword(), keyword()) ::
  {:ok, Xqlite.Result.t()} | error()

Executes a non-returning SQL statement and returns a %Xqlite.Result{}.

For DML statements, changes contains the number of affected rows.

Options

  • :type_extensions — a list of Xqlite.TypeExtension modules; parameters are encoded through the chain before binding (there are no result rows to decode). Default: [].

execute_batch(conn, sql_batch)

@spec execute_batch(conn(), String.t()) :: :ok | error()

Executes a SQL batch (multiple statements separated by semicolons).

Wraps XqliteNIF.execute_batch/2 and emits [:xqlite, :execute_batch, :*] telemetry. No parameter binding inside the batch.

SQLite runs the statements one at a time. The first failure stops the batch and returns that error; the statements that already ran stay applied. There is no implicit transaction around a batch — a batch may open and close its own — so wrap the statements in your own BEGIN / COMMIT when the batch has to be all-or-nothing.

execute_batch_cancellable(conn, sql_batch, token_or_tokens)

@spec execute_batch_cancellable(conn(), String.t(), reference() | [reference()]) ::
  :ok | error()

Cancellable execute_batch/2. Accepts either a single cancel token or a list.

execute_cancellable(conn, sql, params, token_or_tokens)

@spec execute_cancellable(
  conn(),
  String.t(),
  list(),
  reference() | [reference()]
) :: {:ok, non_neg_integer()} | error()

Cancellable execute/3. Accepts either a single cancel token or a list.

explain_analyze(conn, sql, params \\ [])

@spec explain_analyze(conn(), String.t(), list() | keyword()) ::
  {:ok, Xqlite.ExplainAnalyze.t()} | error()

Runs a SQL statement and returns an %Xqlite.ExplainAnalyze{} report.

The statement is executed in full (rows are fetched and discarded). The returned struct combines the static EXPLAIN QUERY PLAN tree with runtime counters from sqlite3_stmt_scanstatus_v2 / sqlite3_stmt_status and a wall-clock measurement around the execution. See Xqlite.ExplainAnalyze for the field layout and how to interpret it.

The SQL must hold exactly one statement, the same rule prepare/2 and query/3 apply: SQL holding no statement at all is {:error, {:cannot_execute, _}} rather than a report of zeroes, and a second statement after the first is {:error, :multiple_statements}.

Examples

iex> {:ok, conn} = Xqlite.open_in_memory()
iex> XqliteNIF.execute_batch(conn, "CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT); INSERT INTO t(name) VALUES ('a'), ('b');")
:ok
iex> {:ok, report} = Xqlite.explain_analyze(conn, "SELECT name FROM t WHERE name = ?", ["b"])
iex> match?(%Xqlite.ExplainAnalyze{}, report)
true

finalize(stmt)

@spec finalize(stmt()) :: :ok | error()

Finalizes a prepared statement, releasing its SQLite resources.

Idempotent — repeated finalization returns :ok. Prefer explicit finalization over relying on garbage collection, and finalize before closing the owning connection (see prepare/2).

get_create_sql(conn, object_name)

@spec get_create_sql(conn(), String.t()) :: {:ok, String.t() | nil} | error()

Returns the CREATE ... SQL for a schema object as recorded in sqlite_schema, or {:ok, nil} when no object with that name exists.

Wraps XqliteNIF.get_create_sql/2. No telemetry is emitted.

get_pragma(conn, name)

@spec get_pragma(conn(), String.t() | atom()) :: {:ok, term()} | error()

Reads a PRAGMA value from the connection.

Wraps XqliteNIF.get_pragma/2 and emits [:xqlite, :pragma, :get].

last_insert_rowid(conn)

@spec last_insert_rowid(conn()) :: {:ok, integer()} | error()

Returns the rowid of the most recent successful INSERT on this connection.

Connection-specific and only updated by successful INSERTs. Does not work for WITHOUT ROWID tables — use INSERT ... RETURNING there. Wraps XqliteNIF.last_insert_rowid/1. No telemetry is emitted.

load_extension(conn, path, entry_point \\ nil)

@spec load_extension(conn(), String.t(), String.t() | nil) :: :ok | error()

Loads a SQLite extension from the shared library at path.

entry_point is the extension's init function name; pass nil (default) to let SQLite auto-detect. Extension loading must be enabled first via enable_load_extension/2.

multi_step(stmt, batch_size)

@spec multi_step(stmt(), pos_integer()) ::
  {:ok, %{rows: [[sqlite_value()]], done: boolean()}} | error()

Advances a prepared statement up to batch_size rows.

Returns {:ok, %{rows: rows, done: done?}}done: true means the statement exhausted within this batch (fewer than batch_size rows may be returned in that case) — or {:error, reason}.

Calling again after done: true without a reset/1 RERUNS the query from the top (v2-prepared statements auto-reset when stepped past done — SQLite semantics, same as step/1).

multi_step_cancellable(stmt, batch_size, token_or_tokens)

@spec multi_step_cancellable(stmt(), pos_integer(), reference() | [reference()]) ::
  {:ok, %{rows: [[sqlite_value()]], done: boolean()}} | error()

Like multi_step/2 but cancellable.

Accepts a single cancel token or a list (OR-semantics — any signalled token aborts with {:error, :operation_cancelled}). Cancellation rides the connection's progress handler, exactly like query_cancellable/4. After a cancellation, reset/1 the statement before stepping it again.

open(path, opts \\ [])

@spec open(
  String.t(),
  keyword()
) :: {:ok, conn()} | error()

Opens a database connection with opinionated defaults and validated options.

All PRAGMAs are applied on the same connection immediately after opening, with no window for another process to observe an unconfigured state.

Options

  • :journal_mode - SQLite journal mode. :wal enables concurrent readers with a single writer. The default value is :wal.

  • :busy_timeout (timeout/0) - Milliseconds to wait when the database is locked. :infinity waits forever. The default value is 5000.

  • :foreign_keys (boolean/0) - Enable foreign key constraint enforcement. SQLite defaults to OFF. The default value is true.

  • :synchronous - Synchronous mode. :normal is safe with WAL and significantly faster than :full. The default value is :normal.

  • :cache_size (integer/0) - Page cache size. Negative values mean KB (e.g., -64000 = 64MB). SQLite default is 2MB. The default value is -64000.

  • :temp_store - Where to store temporary tables and indices. The default value is :memory.

  • :wal_autocheckpoint (non_neg_integer/0) - WAL auto-checkpoint threshold in pages. 0 disables auto-checkpoint. The default value is 1000.

  • :mmap_size (non_neg_integer/0) - Memory-mapped I/O size in bytes. 0 disables mmap. The default value is 0.

  • :auto_vacuum - Auto-vacuum mode. Must be set before creating any tables. The default value is :none.

Examples

{:ok, conn} = Xqlite.open("my.db")
{:ok, conn} = Xqlite.open("my.db", journal_mode: :delete, busy_timeout: 10_000)

open_in_memory(opts \\ [])

@spec open_in_memory(keyword()) :: {:ok, conn()} | error()

Opens an in-memory database with opinionated defaults and validated options.

Accepts the same options as open/2.

open_in_memory_readonly(uri \\ ":memory:")

@spec open_in_memory_readonly(String.t()) :: {:ok, conn()} | error()

Opens a read-only connection to an in-memory SQLite database.

Useful for connecting to a named shared-cache in-memory database opened read-write by another connection — pass its URI as uri, or omit it to open a private (empty) read-only :memory: database.

No PRAGMAs are applied; read-only databases can't persist most settings.

open_readonly(path)

@spec open_readonly(String.t()) :: {:ok, conn()} | error()

Opens a read-only connection to an existing database file.

Fails with a structured error if the file does not exist — read-only opens never create. No PRAGMAs are applied; read-only databases can't persist most settings. Writes fail with {:error, {:read_only_database, extended_code, message}}.

Emits [:xqlite, :open, :start | :stop] telemetry with mode :readonly.

open_temporary()

@spec open_temporary() :: {:ok, conn()} | error()

Opens a connection to a private temporary on-disk database.

SQLite backs it with an anonymous file it removes on close; the database has no path — db_path/1 returns {:ok, nil}.

Emits [:xqlite, :open, :start | :stop] telemetry with mode :temp and path: nil.

prepare(conn, sql)

@spec prepare(conn(), String.t()) :: {:ok, stmt()} | error()

Prepares a manually managed statement.

The lifecycle is prepare/2 → (bind/2step/1 / multi_step/2reset/1)* → finalize/1. Preparing once and rebinding in a loop skips SQL parsing/planning on every iteration — the reason prepared statements exist. For one-shot calls, query/3 and execute/3 remain simpler.

Exactly ONE statement is compiled: SQL holding no statement at all returns {:error, {:cannot_execute, reason}} and a second statement after the first returns {:error, :multiple_statements} — nothing is silently dropped. Text after the first statement counts as a second statement only when it compiles to one, so a trailing comment, extra semicolons and whitespace are accepted. A syntax error returns {:error, {:sql_input_error, %{sql: _, offset: _, code: _, message: _}}}, carrying the byte offset SQLite reports — the same shape query/3 returns for the same SQL.

Closing the connection finalizes any statement still outstanding on it, so the SQLite handle is freed either way; an abandoned statement is finalized by garbage collection. After an explicit Xqlite.close/1 every operation on such a statement returns {:error, :connection_closed} and finalize/1 returns :ok.

step/1 and multi_step/2 are not cancellable. The cancellable forms are multi_step_cancellable/3 for a prepared statement, query_cancellable/4 and friends for one-shot SQL, and stream/4 with its :cancel_tokens option for a stream. No telemetry is emitted for statement-lifecycle operations.

Examples

iex> {:ok, conn} = Xqlite.open_in_memory()
iex> {:ok, 0} = XqliteNIF.execute(conn, "CREATE TABLE pairs (a INTEGER, b TEXT)", [])
iex> {:ok, stmt} = Xqlite.prepare(conn, "INSERT INTO pairs (a, b) VALUES (?1, ?2)")
iex> for {a, b} <- [{1, "one"}, {2, "two"}] do
...>   :ok = Xqlite.bind(stmt, [a, b])
...>   :done = Xqlite.step(stmt)
...>   :ok = Xqlite.reset(stmt)
...> end
[:ok, :ok]
iex> Xqlite.finalize(stmt)
:ok
iex> {:ok, query} = Xqlite.prepare(conn, "SELECT a, b FROM pairs ORDER BY a")
iex> Xqlite.step(query)
{:row, [1, "one"]}
iex> Xqlite.multi_step(query, 10)
{:ok, %{rows: [[2, "two"]], done: true}}
iex> Xqlite.column_names(query)
{:ok, ["a", "b"]}
iex> Xqlite.finalize(query)
:ok

query(conn, sql, params \\ [], opts \\ [])

@spec query(conn(), String.t(), list() | keyword(), keyword()) ::
  {:ok, Xqlite.Result.t()} | error()

Executes a SQL query and returns a %Xqlite.Result{} struct.

For SELECT queries, num_rows is the count of returned rows and changes is 0. For DML (INSERT/UPDATE/DELETE), num_rows is 0 (no result rows) and changes is the number of affected rows.

Uses XqliteNIF.query_with_changes/3 which captures the affected row count atomically inside the connection lock. For zero-overhead access without the changes field, use XqliteNIF.query/3 directly.

Options

  • :type_extensions — a list of Xqlite.TypeExtension modules. Parameters are encoded through the chain before binding and result rows are decoded through it after fetching (first match wins, same semantics as stream/4). Default: [] (values pass through untouched).

query_cancellable(conn, sql, params, token_or_tokens)

@spec query_cancellable(
  conn(),
  String.t(),
  list() | keyword(),
  reference() | [reference()]
) :: {:ok, query_result()} | error()

Cancellable query/3. Accepts either a single cancel token or a list of tokens; OR-semantics — any signalled token interrupts the query.

See XqliteNIF.query_cancellable/4 for the raw NIF (list form only).

query_with_changes_cancellable(conn, sql, params, token_or_tokens)

@spec query_with_changes_cancellable(
  conn(),
  String.t(),
  list() | keyword(),
  reference() | [reference()]
) :: {:ok, map()} | error()

Cancellable query_with_changes/3. Accepts either a single cancel token or a list.

register_busy_observer(conn, pid)

@spec register_busy_observer(conn(), pid()) :: {:ok, non_neg_integer()} | error()

Registers a busy-contention observer on the connection.

Every SQLITE_BUSY callback invocation sends

{:xqlite_busy, retries_so_far, elapsed_ms}

to pid. Any number of observers can be registered — each gets its own handle for unregister_busy_observer/2 — and they fire whether or not a retry policy is installed. Xqlite.Telemetry.bridge/2 can subscribe with hooks: [:busy] to re-emit deliveries as [:xqlite, :hook, :busy] telemetry.

Note — an observer takes the connection's single busy slot

SQLite gives a connection one busy callback, and an observer takes it. Observing does not change how long the connection waits: with no policy installed, the callback keeps waiting up to the busy_timeout that was in effect when the slot was taken (5000 ms on a connection whose timeout you never set), and unregistering the last observer puts that timeout back. The value is read through PRAGMA busy_timeout as the slot is taken; if an authorizer denies that read, no timeout is remembered. While the slot is held, PRAGMA busy_timeout reads 0 — SQLite zeroes it whenever a callback is installed — even though the wait still applies.

Warning — PRAGMA busy_timeout silently replaces the callback

Running PRAGMA busy_timeout = N (or XqliteNIF.set_pragma(conn, "busy_timeout", ms)) as raw SQL replaces our C callback with SQLite's built-in one, and every observer silently stops receiving {:xqlite_busy, …} messages. busy_timeout/2 goes through the slot instead: it keeps your observers and applies the new timeout through them.

register_progress_hook(conn, pid, opts \\ [])

@spec register_progress_hook(conn(), pid(), keyword()) ::
  {:ok, non_neg_integer()} | error()

Registers a progress-tick subscriber on the connection.

After every ~64 SQLite VM instructions × every_n, sends

{:xqlite_progress, count, elapsed_ms}              # tag = nil
{:xqlite_progress, tag, count, elapsed_ms}         # tag set

to pid. count is the per-subscriber decimated counter; elapsed_ms is the wall time since this specific subscriber was registered.

Multiple subscribers can coexist independently — each gets its own opaque handle, and unregistering one never affects another.

Options

  • :every_n (positive integer, default 1000) — emit every Nth progress callback fire. The progress callback fires every 8 SQLite VM instructions (currently fixed); every_n decimates further.
  • :tag (atom, default nil) — included in each emitted message as the second tuple element when set. Useful when a single listener process subscribes to multiple connections and needs to tell them apart without spawning a process per connection.

Returns {:ok, handle} where handle is the value to pass to unregister_progress_hook/2. Returns {:error, reason} on failure.

release_savepoint(conn, name)

@spec release_savepoint(conn(), String.t()) :: :ok | error()

Releases a savepoint. Emits [:xqlite, :savepoint, :release].

remove_authorizer(conn)

@spec remove_authorizer(conn()) :: :ok | error()

Removes any authorizer installed on the connection.

Safe to call when none is installed (no-op). After removal, statement preparation is unrestricted again.

No telemetry is emitted.

remove_busy_policy(conn)

@spec remove_busy_policy(conn()) :: :ok | error()

Removes the busy retry policy from the connection.

Observers registered with register_busy_observer/2 keep receiving {:xqlite_busy, …} messages; without a policy the connection waits up to the busy_timeout that was in effect when the busy slot was taken, then surfaces SQLITE_BUSY. Safe to call when no policy is installed.

reset(stmt)

@spec reset(stmt()) :: :ok | error()

Resets a prepared statement so it can be stepped from the start again.

Bindings are preserved (SQLite semantics); use clear_bindings/1 to drop them to NULL. Always returns :ok for a live statement — sqlite3_reset's return code echoes the most recent step error, not the reset itself.

restore(conn, src_path, schema \\ "main")

@spec restore(conn(), String.t(), String.t()) :: :ok | error()

Restores a schema from a file.

Replaces the named schema (default "main") with the contents of the file at src_path. Existing data in that schema is overwritten.

rollback(conn)

@spec rollback(conn()) :: :ok | error()

Rolls back the current transaction. Emits [:xqlite, :transaction, :rollback] with reason: :user_initiated.

SQLite-internal rollbacks (constraint violations, deferred-FK failures at commit time) surface as errors from commit/1 rather than passing through here — those events come from the register_rollback_hook/2 fan-out instead.

rollback_to_savepoint(conn, name)

@spec rollback_to_savepoint(conn(), String.t()) :: :ok | error()

Rolls back to a savepoint without releasing it. Emits [:xqlite, :savepoint, :rollback_to].

Note: this does NOT invoke SQLite's rollback_hook — that fires only for outer-transaction rollbacks. Use register_rollback_hook/2 for outer rollback observability; this telemetry event is what's available for partial-rollback observability.

savepoint(conn, name)

@spec savepoint(conn(), String.t()) :: :ok | error()

Creates a savepoint with the given name. Emits [:xqlite, :savepoint, :create].

schema_columns(conn, table_name)

@spec schema_columns(conn(), String.t()) ::
  {:ok, [Xqlite.Schema.ColumnInfo.t()]} | error()

Returns column details for a table or view as Xqlite.Schema.ColumnInfo structs (PRAGMA table_xinfo), or {:ok, []} when the table does not exist.

Wraps XqliteNIF.schema_columns/2. No telemetry is emitted.

schema_databases(conn)

@spec schema_databases(conn()) :: {:ok, [Xqlite.Schema.DatabaseInfo.t()]} | error()

Lists all databases attached to the connection as Xqlite.Schema.DatabaseInfo structs (PRAGMA database_list).

Wraps XqliteNIF.schema_databases/1. No telemetry is emitted.

schema_foreign_keys(conn, table_name)

@spec schema_foreign_keys(conn(), String.t()) ::
  {:ok, [Xqlite.Schema.ForeignKeyInfo.t()]} | error()

Returns the foreign keys defined on a table as Xqlite.Schema.ForeignKeyInfo structs (PRAGMA foreign_key_list).

Wraps XqliteNIF.schema_foreign_keys/2. No telemetry is emitted.

schema_index_columns(conn, index_name)

@spec schema_index_columns(conn(), String.t()) ::
  {:ok, [Xqlite.Schema.IndexColumnInfo.t()]} | error()

Returns the columns of an index as Xqlite.Schema.IndexColumnInfo structs (PRAGMA index_xinfo), ordered by position in the index.

Wraps XqliteNIF.schema_index_columns/2. No telemetry is emitted.

schema_indexes(conn, table_name)

@spec schema_indexes(conn(), String.t()) ::
  {:ok, [Xqlite.Schema.IndexInfo.t()]} | error()

Returns all indexes on a table as Xqlite.Schema.IndexInfo structs (PRAGMA index_list), including those backing PRIMARY KEY and UNIQUE constraints.

Wraps XqliteNIF.schema_indexes/2. No telemetry is emitted.

schema_list_objects(conn, schema \\ nil)

@spec schema_list_objects(conn(), String.t() | nil) ::
  {:ok, [Xqlite.Schema.SchemaObjectInfo.t()]} | error()

Lists tables, views, and virtual tables as Xqlite.Schema.SchemaObjectInfo structs (PRAGMA table_list).

schema defaults to nil; pass "main", "temp", or an attached database name for predictable results. Wraps XqliteNIF.schema_list_objects/2. No telemetry is emitted.

serialize(conn, schema \\ "main")

@spec serialize(conn(), String.t()) :: {:ok, binary()} | error()

Serializes a database to a contiguous binary.

Returns a binary snapshot of the entire database — an atomic, point-in-time copy. No pages are locked during serialization.

schema identifies which attached database to serialize. Defaults to "main". Use "temp" for the temp database or the name of an attached database.

set_authorizer(conn, denied_actions)

@spec set_authorizer(conn(), [atom()]) :: :ok | error()

Installs a deny-list authorizer on the connection.

SQLite consults an authorizer callback while preparing every statement. This installs one that denies a fixed set of action kinds: if a statement attempts any denied action, preparation fails and the call returns {:error, {:authorization_denied, extended_code, message}} (the extended code is SQLite's SQLITE_AUTH family). Everything else is allowed.

denied_actions is a list of action-kind atoms. The set mirrors SQLite's authorizer action codes — :select, :read, :insert, :update, :delete, :transaction, :savepoint, :pragma, :attach, :detach, :alter_table, :reindex, :analyze, :function, :recursive, the create_* / drop_* object verbs (:create_table, :drop_index, :create_trigger, :create_view, :create_vtable, :drop_vtable, …) including their *_temp_* variants, and :unknown for action codes a future SQLite reports that this build does not map. An unrecognized atom returns {:error, {:invalid_authorizer_action, atom}} and installs nothing — the list is validated in full before anything changes.

Single slot per connection: a second call replaces the previous list, and remove_authorizer/1 clears it. Both are idempotent.

Limits (v1)

  • Action-kind granularity only. The decision is made purely on the action kind; the table, column, trigger and database arguments SQLite passes to the authorizer are ignored. You cannot (yet) deny DELETE on one table while allowing it on another.
  • Deny-only. An action is allowed or denied (SQLite's DENY). The IGNORE disposition (silently treat the access as a NULL/no-op) is not exposed.

Caveat — denying :pragma disables get_pragma/set_pragma

XqliteNIF.get_pragma/2 and set_pragma/3 run PRAGMA statements, which SQLite authorizes as the :pragma action; the schema-introspection helpers lean on PRAGMAs too. Denying :pragma therefore makes all of them fail with {:error, {:authorization_denied, _, _}}. Deny it only when you intend to lock those paths out as well.

Two reads slip past that deny, because neither runs a PRAGMA statement for SQLite to refuse: get_pragma(conn, :wal_autocheckpoint), whose value comes from xqlite's own WAL callback rather than from SQLite (writing it is still denied), and get_create_sql/2, a SELECT over sqlite_schema that obeys the :read and :select actions instead.

No telemetry is emitted for authorizer install/remove or for denials.

Examples

iex> {:ok, conn} = Xqlite.open_in_memory()
iex> XqliteNIF.execute(conn, "CREATE TABLE t(id INTEGER)", [])
{:ok, 0}
iex> Xqlite.set_authorizer(conn, [:delete])
:ok
iex> match?({:error, {:authorization_denied, _, _}}, XqliteNIF.execute(conn, "DELETE FROM t", []))
true
iex> match?({:ok, _}, XqliteNIF.query(conn, "SELECT id FROM t", []))
true
iex> Xqlite.set_authorizer(conn, [:bogus])
{:error, {:invalid_authorizer_action, :bogus}}
iex> Xqlite.remove_authorizer(conn)
:ok
iex> XqliteNIF.execute(conn, "DELETE FROM t", [])
{:ok, 0}

set_busy_policy(conn, opts \\ [])

@spec set_busy_policy(
  conn(),
  keyword()
) :: :ok | error()

Sets the busy retry POLICY on the connection.

When SQLite encounters a locked database (another writer holds RESERVED+) the policy decides whether to retry or surface SQLITE_BUSY to the caller. The policy is single-slot by design — a retry decision cannot compose. To OBSERVE contention (telemetry, structured logging, adaptive backoff), register any number of subscribers with register_busy_observer/2; the two halves are independent.

Options

  • :max_retries (non-negative integer, default 50) — stop after this many retries and let the caller see SQLITE_BUSY.
  • :max_elapsed_ms (non-negative integer, default 5_000) — wall-time ceiling in milliseconds for a single busy event; the clock resets at the start of each fresh contention, like :max_retries.
  • :sleep_ms (non-negative integer, default 10) — milliseconds to sleep between retries. Zero disables the pause (tight spin; rarely what you want).

Replacing an existing policy is atomic; observers are unaffected.

Note — a busy sleep pins the connection

sleep_ms sleeps on the thread holding the connection mutex, so while a retry waits, that connection is pinned: other operations on the same connection block until the sleep-and-retry resolves. Different connections are unaffected. Budget sleep_ms × max_retries accordingly.

Warning — PRAGMA busy_timeout silently replaces the callback

Running PRAGMA busy_timeout = N (or XqliteNIF.set_pragma(conn, "busy_timeout", ms)) replaces our C callback with SQLite's built-in sleep-and-retry one: the policy stops applying AND every busy observer silently stops receiving {:xqlite_busy, …} messages. No memory is leaked — the internal state is reclaimed on the next slot mutation or connection close. To switch to plain-timeout semantics without surprises, use busy_timeout/2.

set_pragma(conn, name, value)

@spec set_pragma(conn(), String.t() | atom(), term()) :: {:ok, term()} | error()

Sets a PRAGMA value on the connection.

Wraps XqliteNIF.set_pragma/3 and emits [:xqlite, :pragma, :set].

sqlite_version()

@spec sqlite_version() :: {:ok, String.t()} | error()

Returns the version string of the linked SQLite C library.

Needs no connection. Wraps XqliteNIF.sqlite_version/0. No telemetry is emitted.

step(stmt)

@spec step(stmt()) :: {:row, [sqlite_value()]} | :done | error()

Advances a prepared statement one row.

Returns {:row, values}, :done when exhausted, or {:error, reason}. Stepping past :done without a reset/1 returns whatever SQLite reports for the re-step (a fresh automatic rerun on modern SQLite).

stream(conn, sql, params \\ [], opts \\ [])

@spec stream(conn(), String.t(), list() | keyword(), keyword()) ::
  Enumerable.t() | error()

Creates a stream that executes a query and emits rows as string-keyed maps.

This provides a high-level, idiomatic Elixir Stream for processing large result sets without loading them all into memory at once. Rows are fetched from the database in batches as the stream is consumed.

Options

  • :batch_size (integer, default: 500) - The maximum number of rows to fetch from the database in a single batch.
  • :type_extensions (list of modules, default: []) - A list of modules implementing the Xqlite.TypeExtension behaviour. Parameters are encoded before binding, and result values are decoded as rows are fetched. Extensions are applied in list order; the first match wins.
  • :on_error (:raise | :halt | :emit_error, default: :raise) - How a mid-fetch error (e.g. an invalid-UTF-8 TEXT value) is surfaced. The stream's element shape FOLLOWS the mode:

    • :raise (default) - happy path yields raw row maps; a mid-fetch error raises Xqlite.StreamError, whose :reason field holds the structured error term. A failed read can never masquerade as a completed stream.
    • :halt - happy path yields raw row maps; a mid-fetch error is logged and the stream stops. LOSSY: the result set is silently truncated and the consumer receives no error signal.
    • :emit_error - yields a uniformly tagged stream: {:ok, row} for each row, followed by a terminal {:error, reason} on failure. An unsupported value returns {:error, {:invalid_on_error, value}} at stream open.
  • :cancel_tokens (a token or a list of them, default: []) - Tokens from create_cancel_token/0, handed to every fetch this stream makes. Signalling any one of them ends the fetch it lands in with {:error, :operation_cancelled} and closes the stream; that error then follows the :on_error mode above, like any other fetch error. Rows already read in the same batch are discarded with it. Tokens are single-use, so a token you have already signalled kills the next stream you hand it to on its first fetch — create a fresh one per stream. A value that is neither a reference nor a list of references returns {:error, {:invalid_cancel_tokens, value}} at stream open; a reference that is not a cancel token cannot be told apart in Elixir and raises ArgumentError at the first fetch, as every other cancellable entry point does.

Examples

iex> {:ok, conn} = Xqlite.open_in_memory()
iex> XqliteNIF.execute_batch(conn, "CREATE TABLE users(id, name); INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob');")
:ok
iex> Xqlite.stream(conn, "SELECT id, name FROM users;") |> Enum.to_list()
[%{"id" => 1, "name" => "Alice"}, %{"id" => 2, "name" => "Bob"}]

Returns an Enumerable.t() on success or {:error, reason} on setup failure. Callers must pattern-match the result before piping — this is intentional, as returning a stream that silently errors on first consume would hide setup failures (e.g., invalid SQL, closed connection).

The SQL must hold exactly one statement, the same rule prepare/2 and query/3 apply: SQL holding no statement at all — empty, whitespace or comments — is {:error, {:cannot_execute, _}} rather than a stream with no rows, and a second statement after the first is {:error, :multiple_statements} rather than a stream over the first one. A trailing comment, extra semicolons and whitespace are accepted.

Errors that occur during stream consumption (e.g. an invalid-UTF-8 value, or the connection being lost mid-stream) are surfaced according to the :on_error option above — by default they raise Xqlite.StreamError.

total_changes(conn)

@spec total_changes(conn()) :: {:ok, non_neg_integer()} | error()

Returns the total number of rows changed by all INSERT, UPDATE, and DELETE statements since the connection was opened, including changes made by triggers. Wraps XqliteNIF.total_changes/1. No telemetry is emitted.

transaction_status(conn)

@spec transaction_status(conn()) :: {:ok, boolean()} | error()

Returns whether the connection is currently inside a transaction.

{:ok, true} after begin/2 and before commit/1 or rollback/1, {:ok, false} in autocommit mode. Wraps XqliteNIF.transaction_status/1. No telemetry is emitted.

txn_state(conn, schema \\ nil)

@spec txn_state(conn(), term()) :: {:ok, :none | :read | :write | :unknown} | error()

Returns the transaction state of a schema: :none, :read, :write, or :unknown (a future SQLite state not mapped yet).

schema defaults to nil, meaning "main". Wraps XqliteNIF.txn_state/2 (see it for why there is no full five-state lock ladder). No telemetry is emitted.

A schema that is neither a string nor nil returns {:error, {:cannot_execute, reason}}.

unregister_busy_observer(conn, handle)

@spec unregister_busy_observer(conn(), non_neg_integer()) :: :ok | error()

Unregisters a busy-contention observer by handle.

Idempotent — an unknown or already-removed handle is a no-op.

unregister_progress_hook(conn, handle)

@spec unregister_progress_hook(conn(), non_neg_integer()) :: :ok | error()

Unregisters a progress-tick subscriber by handle.

Idempotent — unregistering an unknown handle returns :ok. Returns {:error, :connection_closed} if the connection is closed.

wal_checkpoint(conn, mode \\ :passive, schema \\ "main")

@spec wal_checkpoint(conn(), term(), term()) :: {:ok, map()} | error()

Performs a WAL checkpoint on the connection.

mode is one of :passive (default), :full, :restart, or :truncate. schema is the attached-database name (default "main").

Returns {:ok, %{log_pages, checkpointed_pages, busy?}} on success.

Any other mode, and any schema that is not a string, returns {:error, {:cannot_execute, reason}} naming the accepted values.