Rust NIF (Native Implemented Functions) bridge for LibSQL operations.
This module provides the low-level interface to the Rust-based LibSQL client, exposing both raw NIF functions and high-level Elixir helper functions.
NIF Functions
The NIF functions are implemented in Rust (native/ecto_libsql/src/lib.rs) and
provide direct access to LibSQL operations:
- Connection management:
connect/2,ping/1,close/2 - Query execution:
query_args/5,execute_with_transaction/3 - Transaction control:
begin_transaction_with_behavior/2,commit_or_rollback_transaction/5 - Prepared statements:
prepare_statement/2,query_prepared/5,execute_prepared/6 - Batch operations:
execute_batch/4,execute_transactional_batch/4 - Metadata:
last_insert_rowid/1,changes/1,total_changes/1,is_autocommit/1 - Cursors:
declare_cursor/3,fetch_cursor/2 - Sync:
do_sync/2
Helper Functions
High-level Elixir wrappers that provide ergonomic interfaces:
query/3,execute_non_trx/3,execute_with_trx/3- Query executionbegin/2,commit/1,rollback/1- Transaction managementprepare/2,execute_stmt/4,query_stmt/3,close_stmt/1- Prepared statementsbatch/2,batch_transactional/2- Batch operationsget_last_insert_rowid/1,get_changes/1,get_total_changes/1,get_is_autocommit/1- Metadatavector/1,vector_type/2,vector_distance_cos/2- Vector search helperssync/1- Manual replica sync
Thread Safety
The Rust implementation uses thread-safe registries (using Mutex<HashMap>)
to manage connections, transactions, statements, and cursors. Each is
identified by a UUID for safe concurrent access.
Summary
Functions
Install an authorizer hook for row-level security.
Install an update hook for monitoring database changes (CDC).
Execute a batch of SQL statements. Each statement is executed independently. Returns a list of results for each statement.
Execute a batch of SQL statements in a transaction. All statements are executed atomically - if any statement fails, all changes are rolled back.
Begin a new transaction with optional behaviour control.
Set the busy timeout for the connection.
Clear the parameter name cache.
Close a prepared statement and free its resources.
Commit the current transaction.
Create a savepoint within a transaction.
Detects the SQL command type from a query string.
Enable or disable loading of SQLite extensions.
Execute multiple SQL statements from a semicolon-separated string.
Execute a prepared statement with arguments.
Execute multiple SQL statements atomically in a transaction.
Flush the replicator, pushing pending writes to the remote database.
Freeze a remote replica, converting it to a standalone local database.
Get the number of rows modified by the last INSERT, UPDATE or DELETE statement.
Get the current replication frame number from a remote replica.
Check if the connection is in autocommit mode (not in a transaction).
Get the rowid of the last inserted row.
Get the highest frame number from write operations on this database.
Get column metadata for a prepared statement.
Get the total number of rows modified, inserted or deleted since the database connection was opened.
Interrupt any ongoing operation on this connection.
Load a SQLite extension from a dynamic library file.
Get the highest frame number from write operations (for read-your-writes consistency).
Normalise query arguments to a positional parameter list.
Get the current size of the parameter name cache.
Prepare a SQL statement for later execution. Returns a statement ID that can be reused.
Query using a prepared statement (for SELECT queries). Returns the result set.
Release (commit) a savepoint, making its changes permanent within the transaction.
Remove the update hook from a connection.
Reset the connection to a clean state.
Reset a prepared statement to its initial state for reuse.
Roll back the current transaction.
Rollback to a savepoint, undoing all changes made after the savepoint was created.
Get the number of columns in a prepared statement's result set.
Get the name of a column in a prepared statement by its index.
Get the number of parameters in a prepared statement.
Get the name of a parameter in a prepared statement by its index.
Manually trigger a sync for embedded replicas.
Sync a remote replica until a specific frame number is reached.
Create a vector from a list of numbers for use in vector columns.
Generate SQL for cosine distance vector similarity search.
Helper to create a vector column definition for CREATE TABLE.
Functions
Install an authorizer hook for row-level security.
NOT SUPPORTED - Authorizer hooks require synchronous bidirectional communication between Rust and Elixir, which is not feasible with Rustler's threading model.
Why Not Supported
SQLite's authorizer callback is called synchronously during query compilation and expects an immediate response (Allow/Deny/Ignore). This would require:
- Sending a message from Rust to Elixir
- Blocking the Rust thread waiting for a response
- Receiving the response from Elixir
This pattern is not safe with Rustler because:
- The callback runs on a SQLite thread (potentially holding locks)
- Blocking on Erlang scheduler threads can cause deadlocks
- No safe way to do synchronous Rust→Elixir→Rust calls
Alternatives
For row-level security and access control, consider:
Application-level authorization - Check permissions in Elixir before queries:
defmodule MyApp.Auth do def can_access?(user, table, action) do
# Check user permissionsend end
def get_user(id, current_user) do if MyApp.Auth.can_access?(current_user, "users", :read) do
Repo.get(User, id)else
{:error, :unauthorized}end end
Database views - Create views with WHERE clauses for different user levels:
CREATE VIEW user_visible_posts AS SELECT * FROM posts WHERE user_id = current_user_id();
Query rewriting - Modify queries in Elixir to include authorization constraints:
defmodule MyApp.Repo do def all(queryable, current_user) do
queryable |> apply_tenant_filter(current_user) |> Ecto.Repo.all()end end
Connection-level restrictions - Use different database connections with different privileges
Returns
:unsupported- Always returns unsupported
Install an update hook for monitoring database changes (CDC).
NOT SUPPORTED - Update hooks require sending messages from managed BEAM threads, which is not allowed by Rustler's threading model.
Why Not Supported
SQLite's update hook callback is called synchronously during INSERT/UPDATE/DELETE operations, and runs on the same thread executing the SQL statement. In our NIF implementation:
- SQL execution happens on Erlang scheduler threads (managed by BEAM)
- Rustler's
OwnedEnv::send_and_clear()can ONLY be called from unmanaged threads - Calling
send_and_clear()from a managed thread causes a panic
This is a fundamental limitation of mixing NIF callbacks with Erlang's threading model.
Alternatives
For change data capture and real-time updates, consider:
Application-level events - Emit events from your Ecto repos:
defmodule MyApp.Repo do def insert(changeset, opts \ []) do
case Ecto.Repo.insert(__MODULE__, changeset, opts) do {:ok, record} = result -> Phoenix.PubSub.broadcast(MyApp.PubSub, "db_changes", {:insert, record}) result error -> error endend end
Database triggers - Use SQLite triggers to log changes to a separate table:
CREATE TRIGGER users_audit_insert AFTER INSERT ON users BEGIN INSERT INTO audit_log (action, table_name, row_id, timestamp) VALUES ('insert', 'users', NEW.id, datetime('now')); END;
Polling-based CDC - Periodically query for changes using timestamps or version columns
Phoenix.Tracker - Track state changes at the application level
Returns
:unsupported- Always returns unsupported
@spec batch(EctoLibSql.State.t(), [{String.t(), list()}]) :: {:ok, [EctoLibSql.Result.t()]} | {:error, term()}
Execute a batch of SQL statements. Each statement is executed independently. Returns a list of results for each statement.
Parameters
- state: The connection state
- statements: A list of tuples {sql, args} where sql is the SQL string and args is a list of parameters
Example
statements = [
{"INSERT INTO users (name) VALUES (?)", ["Alice"]},
{"INSERT INTO users (name) VALUES (?)", ["Bob"]},
{"SELECT * FROM users", []}
]
{:ok, results} = EctoLibSql.Native.batch(state, statements)
@spec batch_transactional(EctoLibSql.State.t(), [{String.t(), list()}]) :: {:ok, [EctoLibSql.Result.t()]} | {:error, term()}
Execute a batch of SQL statements in a transaction. All statements are executed atomically - if any statement fails, all changes are rolled back.
Parameters
- state: The connection state
- statements: A list of tuples {sql, args} where sql is the SQL string and args is a list of parameters
Example
statements = [
{"INSERT INTO users (name) VALUES (?)", ["Alice"]},
{"INSERT INTO users (name) VALUES (?)", ["Bob"]},
{"UPDATE users SET active = 1", []}
]
{:ok, results} = EctoLibSql.Native.batch_transactional(state, statements)
@spec begin(EctoLibSql.State.t(), Keyword.t()) :: {:ok, EctoLibSql.State.t()} | {:error, term()}
Begin a new transaction with optional behaviour control.
Parameters
- state: The connection state
- opts: Options keyword list
:behavior- Transaction behaviour (:deferred,:immediate, or:exclusive), defaults to:deferred
Transaction Behaviours
:deferred- Default. Locks are acquired on first write operation:immediate- Acquires write lock immediately when transaction begins:exclusive- Acquires exclusive lock immediately, blocking all other connections
Example
{:ok, new_state} = EctoLibSql.Native.begin(state, behavior: :immediate)
Set the busy timeout for the connection.
This controls how long SQLite waits when a table is locked before returning a SQLITE_BUSY error. By default, SQLite returns immediately when encountering a lock. Setting a timeout allows for better concurrency handling.
Parameters
- state: The connection state
- timeout_ms: Timeout in milliseconds (default: 5000)
Example
# Set 5 second timeout (recommended default)
:ok = EctoLibSql.Native.busy_timeout(state, 5000)
# Set 10 second timeout for write-heavy workloads
:ok = EctoLibSql.Native.busy_timeout(state, 10_000)Notes
- A value of 0 disables the busy handler (immediate SQLITE_BUSY on contention)
- Recommended production default is 5000ms (5 seconds)
- For write-heavy workloads, consider 10000ms or higher
@spec clear_param_cache() :: :ok
Clear the parameter name cache.
The cache stores SQL statements and their parameter name mappings to avoid repeated introspection overhead. Each entry contains the full SQL string, parameter names list, and access timestamp.
Use this function to:
- Reclaim memory in applications with many dynamic queries
- Reset cache state during testing
- Force re-introspection after schema changes
The cache will be automatically rebuilt as queries are executed.
Use param_cache_size/0 to monitor cache utilisation before clearing.
Close a prepared statement and free its resources.
Parameters
- stmt_id: The statement ID to close
Example
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT * FROM users WHERE id = ?")
# ... use statement ...
:ok = EctoLibSql.Native.close_stmt(stmt_id)
@spec commit(EctoLibSql.State.t()) :: {:ok, String.t()} | {:error, term()}
Commit the current transaction.
For embedded replicas with auto-sync enabled, this also triggers a sync.
Parameters
- state: The connection state with an active transaction
Example
{:ok, _} = EctoLibSql.Native.commit(state)
Create a savepoint within a transaction.
Savepoints allow partial rollback without aborting the entire transaction. They enable nested transaction-like behaviour.
Parameters
- state: The connection state with an active transaction
- name: The savepoint name (must be unique within the transaction)
Example
{:ok, trx_state} = EctoLibSql.Native.begin(state)
:ok = EctoLibSql.Native.create_savepoint(trx_state, "sp1")
# Do some work...
{:ok, _query, _result, trx_state} = EctoLibSql.Native.execute_with_trx(trx_state, "INSERT INTO users VALUES (?)", ["Alice"])
# Create nested savepoint
:ok = EctoLibSql.Native.create_savepoint(trx_state, "sp2")Notes
- Savepoints must be created within an active transaction
- Savepoint names must be valid SQL identifiers
- You can create nested savepoints
@spec detect_command(String.t()) :: EctoLibSql.Result.command_type()
Detects the SQL command type from a query string.
Returns an atom representing the command type, or :unknown for
unrecognised commands.
Examples
iex> EctoLibSql.Native.detect_command("SELECT * FROM users")
:select
iex> EctoLibSql.Native.detect_command("INSERT INTO users VALUES (1)")
:insert
Enable or disable loading of SQLite extensions.
By default, extension loading is disabled for security reasons.
You must explicitly enable it before calling load_ext/3.
Parameters
- state: The connection state
- enabled: Whether to enable (true) or disable (false) extension loading
Returns
:ok- Extension loading enabled/disabled successfully{:error, reason}- Operation failed
Example
# Enable extension loading
:ok = EctoLibSql.Native.enable_extensions(state, true)
# Load an extension
:ok = EctoLibSql.Native.load_ext(state, "/path/to/extension.so")
# Disable extension loading (recommended after loading)
:ok = EctoLibSql.Native.enable_extensions(state, false)Security Warning
⚠️ Only enable extension loading if you trust the extensions being loaded. Malicious extensions can compromise database security and execute arbitrary code.
Execute multiple SQL statements from a semicolon-separated string.
Uses LibSQL's native batch execution for optimal performance. This is more efficient than executing statements one-by-one as it reduces round-trips and allows LibSQL to optimize the execution.
Each statement is executed independently. If one fails, others may still complete.
Parameters
- state: The connection state
- sql: Semicolon-separated SQL statements
Example
sql = """
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO users (name) VALUES ('Alice');
INSERT INTO users (name) VALUES ('Bob');
SELECT * FROM users;
"""
{:ok, results} = EctoLibSql.Native.execute_batch_sql(state, sql)Returns
A list of results, one for each statement. Each result is either:
- A map with columns/rows for SELECT statements
nilfor statements that don't return data
Execute a prepared statement with arguments.
Automatically routes to query_stmt if the statement returns rows (e.g., SELECT, EXPLAIN, RETURNING), or to execute_prepared if it doesn't (e.g., INSERT/UPDATE/DELETE without RETURNING).
Parameters
- state: The connection state
- stmt_id: The statement ID from prepare/2
- sql: The original SQL (for sync detection and statement type detection)
- args: List of positional parameters OR map with atom keys for named parameters
Examples
# INSERT without RETURNING
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "INSERT INTO users (name) VALUES (?)")
{:ok, num_rows} = EctoLibSql.Native.execute_stmt(state, stmt_id, sql, ["Alice"])
# SELECT query
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT * FROM users WHERE id = ?")
{:ok, result} = EctoLibSql.Native.execute_stmt(state, stmt_id, sql, [1])
# EXPLAIN query
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "EXPLAIN QUERY PLAN SELECT * FROM users")
{:ok, result} = EctoLibSql.Native.execute_stmt(state, stmt_id, sql, [])
# INSERT with RETURNING
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "INSERT INTO users (name) VALUES (?) RETURNING *")
{:ok, result} = EctoLibSql.Native.execute_stmt(state, stmt_id, sql, ["Alice"])
Execute multiple SQL statements atomically in a transaction.
Uses LibSQL's native transactional batch execution. All statements execute within a single transaction - if any statement fails, all changes are rolled back.
Parameters
- state: The connection state
- sql: Semicolon-separated SQL statements
Example
sql = """
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 2, 100);
"""
{:ok, results} = EctoLibSql.Native.execute_transactional_batch_sql(state, sql)Notes
- All statements succeed or all are rolled back
- More efficient than manual transaction with multiple queries
- Ideal for migrations, data loading, and multi-statement operations
Flush the replicator, pushing pending writes to the remote database.
This forces the local replica to synchronize with the remote database, sending any pending local changes.
Parameters
- conn_id: The connection ID
Returns
{:ok, new_frame}- Flush succeeded, returns new frame number{:error, reason}- If flush failed
Example
{:ok, frame} = EctoLibSql.Native.flush_and_get_frame(replica_conn_id)
Logger.info("Flushed to frame: " <> to_string(frame))Notes
- This is useful before taking snapshots or backups
- Returns the frame number after the flush (0 if not a replica)
- For local or remote primary connections, returns 0
Freeze a remote replica, converting it to a standalone local database.
⚠️ NOT SUPPORTED - This function is currently not implemented.
Freeze is intended to convert a remote replica to a standalone local database for disaster recovery. However, this operation requires deep refactoring of the connection pool architecture and remains unimplemented. Instead, you can:
- Option 1: Backup the replica database file and use it independently
- Option 2: Replicate all data to a new local database
- Option 3: Keep the replica and manage failover at the application level
Always returns {:error, :unsupported}.
Parameters
- state: The connection state
Returns
{:error, :unsupported}- Always (not implemented)
Example
case EctoLibSql.Native.freeze_replica(replica_state) do
{:ok, _frozen_state} ->
# This will never succeed
:unreachable
{:error, :unsupported} ->
Logger.error("Freeze is not supported. Use manual backup strategy instead.")
{:error, :unsupported}
endImplementation Status
- Blocker: Requires taking ownership of the
Databaseinstance, which is held inArc<Mutex<LibSQLConn>>within connection pool state - Work Required: Refactoring connection pool architecture to support consuming connections
- Timeline: Uncertain - marked for future refactoring
See CLAUDE.md for technical details on why this is not currently supported.
Get the number of rows modified by the last INSERT, UPDATE or DELETE statement.
Parameters
- state: The connection state
Example
{:ok, _result, state} = EctoLibSql.Native.execute_non_trx(query, state, [])
num_changes = EctoLibSql.Native.get_changes(state)
Get the current replication frame number from a remote replica.
This returns the current frame number at the local replica, useful for monitoring replication progress. The frame number increases with each replication event.
Parameters
- conn_id: The connection ID (usually state.conn_id)
Returns
{:ok, frame_no}- The current frame number (0 if not a replica){:error, reason}- If the connection is invalid
Example
{:ok, frame_no} = EctoLibSql.Native.get_frame_number_for_replica(state.conn_id)
Logger.info("Current replication frame: " <> to_string(frame_no))Notes
- Returns 0 if the database is not a remote replica
- For local databases, this is not applicable
- Useful for implementing replication lag monitoring
Check if the connection is in autocommit mode (not in a transaction).
Parameters
- state: The connection state
Example
autocommit? = EctoLibSql.Native.get_is_autocommit(state)
Get the rowid of the last inserted row.
Parameters
- state: The connection state
Example
{:ok, _result, state} = EctoLibSql.Native.execute_non_trx(query, state, ["Alice"])
rowid = EctoLibSql.Native.get_last_insert_rowid(state)
Get the highest frame number from write operations on this database.
This is useful for read-your-writes consistency across replicas. After
performing writes on one connection (typically a primary or another replica),
you can use this function to get the maximum write frame, then use
sync_until_frame/2 on other replicas to ensure they've synced up to at
least that frame before reading.
Parameters
- conn_id: The connection ID
Returns
{:ok, frame_no}- The highest frame number from write operations (0 if no writes tracked){:error, reason}- If the connection is invalid
Example
# On primary/writer connection, after writes
{:ok, max_write_frame} = EctoLibSql.Native.get_max_write_frame(primary_conn_id)
# On replica connection, ensure it's synced to at least that frame
:ok = EctoLibSql.Native.sync_until_frame(replica_conn_id, max_write_frame)
# Now safe to read from replica - guaranteed to see writes from primaryNotes
- Returns 0 if the database doesn't track write replication index
- Different from
get_frame_number_for_replica/1which returns current replication position - This tracks the highest frame number from YOUR write operations
- Essential for read-your-writes consistency in multi-replica setups
Get column metadata for a prepared statement.
Returns information about all columns that will be returned when the statement is executed. This includes column names, origin names, and declared types.
Parameters
- state: The connection state with the prepared statement
- stmt_id: The prepared statement ID
Returns
{:ok, columns}- List of tuples with{name, origin_name, decl_type}{:error, reason}- Failed to get metadata
Example
{:ok, stmt_id} = EctoLibSql.prepare(state, "SELECT id, name, age FROM users")
{:ok, columns} = EctoLibSql.Native.get_stmt_columns(state, stmt_id)
# Returns:
# [
# {"id", "id", "INTEGER"},
# {"name", "name", "TEXT"},
# {"age", "age", "INTEGER"}
# ]Use Cases
- Type introspection: Understand column types for dynamic queries
- Schema discovery: Explore database structure without separate queries
- Better error messages: Show column names and types in error output
- Type casting hints: Help Ecto determine appropriate type conversions
Get the total number of rows modified, inserted or deleted since the database connection was opened.
Parameters
- state: The connection state
Example
total = EctoLibSql.Native.get_total_changes(state)
Interrupt any ongoing operation on this connection.
Causes the current database operation to abort and return at the earliest opportunity. Useful for:
- Cancelling long-running queries
- Implementing query timeouts
- Graceful shutdown
Parameters
- state: The connection state
Example
# From another process, cancel a long query
:ok = EctoLibSql.Native.interrupt(state)Notes
- This is safe to call from any thread/process
- The interrupted operation will return an error
Load a SQLite extension from a dynamic library file.
Extensions must be enabled first via enable_extensions/2.
Parameters
- state: The connection state
- path: Path to the extension dynamic library (.so, .dylib, or .dll)
- entry_point: Optional entry point function name (defaults to extension-specific default)
Returns
:ok- Extension loaded successfully{:error, reason}- Extension loading failed
Example
# Enable extension loading first
:ok = EctoLibSql.Native.enable_extensions(state, true)
# Load an extension
:ok = EctoLibSql.Native.load_ext(state, "/usr/lib/sqlite3/pcre.so")
# Load with custom entry point
:ok = EctoLibSql.Native.load_ext(state, "/path/to/extension.so", "sqlite3_extension_init")
# Disable extension loading after
:ok = EctoLibSql.Native.enable_extensions(state, false)Common Extensions
- FTS5 (full-text search) - Usually built-in, provides advanced full-text search
- JSON1 (JSON functions) - Usually built-in, provides JSON manipulation functions
- R-Tree (spatial indexing) - Spatial data structures for geographic data
- PCRE (regular expressions) - Perl-compatible regular expressions
- Custom user-defined functions
Security Warning
⚠️ Only load extensions from trusted sources. Extensions run with full database access and can execute arbitrary code.
Notes
- Extension loading must be enabled first via
enable_extensions/2 - Extensions are loaded per-connection, not globally
- Some extensions may already be built into libsql (FTS5, JSON1)
- Extension files must match your platform (.so on Linux, .dylib on macOS, .dll on Windows)
Get the highest frame number from write operations (for read-your-writes consistency).
This is a low-level NIF function that returns the maximum replication frame
number from write operations on this database connection. It's primarily used
internally by get_max_write_frame/1.
For most use cases, use get_max_write_frame/1 instead, which provides better
error handling and documentation.
Parameters
- conn_id: The connection ID (string)
Returns
- Integer frame number (0 if no writes tracked)
{:error, reason}if the connection is invalid
Notes
- This is a raw NIF function - prefer
get_max_write_frame/1for normal usage - Returns 0 for local databases (not applicable)
- Frame number increases with each write operation
- Essential for implementing read-your-writes consistency in multi-replica setups
Normalise query arguments to a positional parameter list.
Arguments
conn_id- The connection identifierstatement- The SQL statement (used for named parameter introspection)args- The arguments to normalise; must be a list or map
Returns
list- Positional parameter list on success{:error, reason}- Error tuple if args is invalid or map conversion fails
Accepted Types
- List: Returned as-is (positional parameters)
- Map: Converted to positional list using statement parameter introspection
Any other type returns {:error, "arguments must be a list or map"}.
@spec param_cache_size() :: non_neg_integer()
Get the current size of the parameter name cache.
Returns the number of cached SQL statement parameter mappings. The cache has a maximum size of 1000 entries.
Useful for monitoring cache utilisation in applications with dynamic queries. If the cache frequently hits the maximum, consider whether query patterns could be optimised to reduce unique SQL variations.
Prepare a SQL statement for later execution. Returns a statement ID that can be reused.
Parameters
- state: The connection state
- sql: The SQL query to prepare
Example
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT * FROM users WHERE id = ?")
{:ok, result} = EctoLibSql.Native.query_stmt(state, stmt_id, [42])
Query using a prepared statement (for SELECT queries). Returns the result set.
Parameters
- state: The connection state
- stmt_id: The statement ID from prepare/2
- args: List of positional parameters OR map with atom keys for named parameters
Examples
# Positional parameters
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT * FROM users WHERE id = ?")
{:ok, result} = EctoLibSql.Native.query_stmt(state, stmt_id, [42])
# Named parameters with atom keys
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT * FROM users WHERE id = :id")
{:ok, result} = EctoLibSql.Native.query_stmt(state, stmt_id, %{id: 42})
Release (commit) a savepoint, making its changes permanent within the transaction.
Parameters
- state: The connection state with an active transaction
- name: The savepoint name to release
Example
{:ok, trx_state} = EctoLibSql.Native.begin(state)
:ok = EctoLibSql.Native.create_savepoint(trx_state, "sp1")
# ... do work ...
:ok = EctoLibSql.Native.release_savepoint_by_name(trx_state, "sp1")
Remove the update hook from a connection.
NOT SUPPORTED - Update hooks are not currently implemented.
Returns
:unsupported- Always returns unsupported
Reset the connection to a clean state.
This clears any cached state and resets the connection. Useful for:
- Connection pooling (ensuring clean state when returning to pool)
- Recovering from errors
- Clearing any uncommitted transaction state
Parameters
- state: The connection state
Example
:ok = EctoLibSql.Native.reset(state)
Reset a prepared statement to its initial state for reuse.
After executing a statement, you should reset it before binding new parameters and executing again. This allows efficient statement reuse without re-preparing the same SQL string repeatedly.
Performance Note: Resetting and reusing statements is 10-15x faster than re-preparing the same SQL string. Always reset statements when executing the same query multiple times with different parameters.
Parameters
- state: The connection state with the prepared statement
- stmt_id: The prepared statement ID
Returns
:ok- Statement reset successfully{:error, reason}- Reset failed
Example
{:ok, stmt_id} = EctoLibSql.prepare(state, "INSERT INTO logs (msg) VALUES (?)")
for msg <- messages do
EctoLibSql.execute_stmt(state, stmt_id, [msg])
EctoLibSql.Native.reset_stmt(state, stmt_id) # Reset for next iteration
end
EctoLibSql.close_stmt(state, stmt_id)
@spec rollback(EctoLibSql.State.t()) :: {:ok, String.t()} | {:error, term()}
Roll back the current transaction.
Parameters
- state: The connection state with an active transaction
Example
{:ok, _} = EctoLibSql.Native.rollback(state)
Rollback to a savepoint, undoing all changes made after the savepoint was created.
The savepoint remains active after rollback and can be released or rolled back to again. The transaction itself remains active.
Parameters
- state: The connection state with an active transaction
- name: The savepoint name to rollback to
Example
{:ok, trx_state} = EctoLibSql.Native.begin(state)
{:ok, _query, _result, trx_state} = EctoLibSql.Native.execute_with_trx(trx_state, "INSERT INTO users VALUES (?)", ["Alice"])
:ok = EctoLibSql.Native.create_savepoint(trx_state, "sp1")
{:ok, _query, _result, trx_state} = EctoLibSql.Native.execute_with_trx(trx_state, "INSERT INTO users VALUES (?)", ["Bob"])
# Rollback Bob insert, keep Alice
:ok = EctoLibSql.Native.rollback_to_savepoint_by_name(trx_state, "sp1")
# Transaction still active, can continue or commit
:ok = EctoLibSql.Native.commit(trx_state)
Get the number of columns in a prepared statement's result set.
Returns the column count for statements that return rows (SELECT). Returns 0 for statements that don't return rows (INSERT, UPDATE, DELETE).
Parameters
- state: The connection state
- stmt_id: The statement ID returned from
prepare/2
Example
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT id, name, email FROM users")
{:ok, count} = EctoLibSql.Native.stmt_column_count(state, stmt_id)
# count = 3
Get the name of a column in a prepared statement by its index.
Index is 0-based. Returns an error if the index is out of bounds.
Parameters
- state: The connection state
- stmt_id: The statement ID returned from
prepare/2 - idx: Column index (0-based)
Example
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT id, name FROM users")
{:ok, name} = EctoLibSql.Native.stmt_column_name(state, stmt_id, 0)
# name = "id"
{:ok, name} = EctoLibSql.Native.stmt_column_name(state, stmt_id, 1)
# name = "name"
Get the number of parameters in a prepared statement.
Parameters are the placeholders (?) in the SQL statement.
Parameters
- state: The connection state
- stmt_id: The statement ID returned from
prepare/2
Example
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT * FROM users WHERE id = ? AND name = ?")
{:ok, count} = EctoLibSql.Native.stmt_parameter_count(state, stmt_id)
# count = 2
Get the name of a parameter in a prepared statement by its index.
Returns the parameter name for named parameters (:name, @name, $name),
or nil for positional parameters (?).
Parameters
- state: The connection state
- stmt_id: The statement ID returned from
prepare/2 - idx: Parameter index (1-based, following SQLite convention)
Returns
{:ok, name}- Parameter has a name (e.g.,:idreturns"id"){:ok, nil}- Parameter is positional (?){:error, reason}- Error occurred
Example
# Named parameters
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT * FROM users WHERE id = :id AND name = :name")
{:ok, param1} = EctoLibSql.Native.stmt_parameter_name(state, stmt_id, 1)
# param1 = "id"
{:ok, param2} = EctoLibSql.Native.stmt_parameter_name(state, stmt_id, 2)
# param2 = "name"
# Positional parameters
{:ok, stmt_id} = EctoLibSql.Native.prepare(state, "SELECT * FROM users WHERE id = ?")
{:ok, param1} = EctoLibSql.Native.stmt_parameter_name(state, stmt_id, 1)
# param1 = nilNotes
- Parameter indices are 1-based (first parameter is index 1)
- Named parameters start with
:,@, or$in SQL but the prefix is stripped in the returned name - Returns
nilfor positional?placeholders
@spec sync(EctoLibSql.State.t()) :: {:ok, String.t()} | {:error, term()}
Manually trigger a sync for embedded replicas.
For connections in :remote_replica mode, this function forces a
synchronisation with the remote Turso database, pulling down any changes
from the remote and pushing local changes up.
When to Use
In most cases, you don't need to call this manually - automatic sync happens
when you connect with sync: true. However, manual sync is useful for:
- Critical reads after remote writes: When you need to immediately read data that was just written to the remote database
- Before shutdown: Ensuring all local changes are synced before closing the connection
- After batch operations: Forcing sync after bulk inserts/updates to ensure data is persisted remotely
- Coordinating between replicas: When multiple replicas need to see consistent data immediately
Parameters
- state: The connection state (must be in
:remote_replicamode)
Returns
{:ok, "success sync"}on successful sync{:error, reason}if sync fails
Examples
# Force sync after critical write
{:ok, state} = EctoLibSql.connect(database: "local.db", uri: turso_uri, auth_token: token, sync: true)
{:ok, _, _, state} = EctoLibSql.handle_execute("INSERT INTO users ...", [], [], state)
{:ok, "success sync"} = EctoLibSql.Native.sync(state)
# Ensure sync before shutdown
{:ok, _} = EctoLibSql.Native.sync(state)
:ok = EctoLibSql.disconnect([], state)Notes
- Sync is only applicable for
:remote_replicamode connections - For
:localmode, this is a no-op - For
:remotemode, data is already on the remote server - Sync happens synchronously and may take time depending on data size
Sync a remote replica until a specific frame number is reached.
Waits for the replica to catch up to the specified frame number, which is useful after bulk writes to the primary database.
Parameters
- conn_id: The connection ID
- target_frame: The target frame number to sync until
Returns
:ok- Successfully synced to the target frame{:error, reason}- If sync failed or connection is invalid
Example
# After bulk insert on primary, wait for replica to catch up
primary_frame = get_primary_frame_number()
:ok = EctoLibSql.Native.sync_until_frame(replica_conn_id, primary_frame)
# Replica is now up-to-dateNotes
- This blocks until the frame is reached (with internal timeout)
- Only works for remote replica connections
- Returns error if called on local or remote primary connections
Create a vector from a list of numbers for use in vector columns.
Parameters
- values: List of numbers (integers or floats)
Example
# Create a 3-dimensional vector
vec = EctoLibSql.Native.vector([1.0, 2.0, 3.0])
# Use in query: "INSERT INTO items (embedding) VALUES (?)"
Generate SQL for cosine distance vector similarity search.
Parameters
- column: Name of the vector column
- vector: The query vector (list of numbers or vector string)
Example
distance_sql = EctoLibSql.Native.vector_distance_cos("embedding", [1.0, 2.0, 3.0])
# Returns: "vector_distance_cos(embedding, '[1.0,2.0,3.0]')"
# Use in: "SELECT * FROM items ORDER BY #{distance_sql} LIMIT 10"
Helper to create a vector column definition for CREATE TABLE.
Parameters
- dimensions: Number of dimensions
- type: :f32 (float32) or :f64 (float64), defaults to :f32
Example
column_def = EctoLibSql.Native.vector_type(3) # "F32_BLOB(3)"
# Use in: "CREATE TABLE items (embedding #{column_def})"