# `EctoLibSql.Native`
[🔗](https://github.com/ocean/ecto_libsql/blob/v0.9.1/lib/ecto_libsql/native.ex#L1)

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 execution
- `begin/2`, `commit/1`, `rollback/1` - Transaction management
- `prepare/2`, `execute_stmt/4`, `query_stmt/3`, `close_stmt/1` - Prepared statements
- `batch/2`, `batch_transactional/2` - Batch operations
- `get_last_insert_rowid/1`, `get_changes/1`, `get_total_changes/1`, `get_is_autocommit/1` - Metadata
- `vector/1`, `vector_type/2`, `vector_distance_cos/2` - Vector search helpers
- `sync/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.

# `add_authorizer`

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:
1. Sending a message from Rust to Elixir
2. Blocking the Rust thread waiting for a response
3. 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:

1. **Application-level authorization** - Check permissions in Elixir before queries:

    defmodule MyApp.Auth do
      def can_access?(user, table, action) do
        # Check user permissions
      end
    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

2. **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();

3. **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

4. **Connection-level restrictions** - Use different database connections with different privileges

## Returns
  - `:unsupported` - Always returns unsupported

# `add_update_hook`

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:
1. SQL execution happens on Erlang scheduler threads (managed by BEAM)
2. Rustler's `OwnedEnv::send_and_clear()` can ONLY be called from unmanaged threads
3. 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:

1. **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
        end
      end
    end

2. **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;

3. **Polling-based CDC** - Periodically query for changes using timestamps or version columns

4. **Phoenix.Tracker** - Track state changes at the application level

## Returns
  - `:unsupported` - Always returns unsupported

# `batch`

```elixir
@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)

# `batch_transactional`

```elixir
@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)

# `begin`

```elixir
@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)

# `busy_timeout`

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

# `clear_param_cache`

```elixir
@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_stmt`

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)

# `commit`

```elixir
@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_savepoint`

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

# `detect_command`

```elixir
@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_extensions`

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_batch_sql`

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
- `nil` for statements that don't return data

# `execute_stmt`

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_transactional_batch_sql`

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_and_get_frame`

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_replica`

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}
    end

## Implementation Status

- **Blocker**: Requires taking ownership of the `Database` instance, which is
  held in `Arc<Mutex<LibSQLConn>>` within connection pool state
- **Work Required**: Refactoring connection pool architecture to support
  consuming connections
- **Timeline**: Uncertain - marked for future refactoring

See the [architecture notes](https://github.com/ocean/ecto_libsql/blob/main/CLAUDE.md) for technical details on why this is not currently supported.

# `get_changes`

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_frame_number_for_replica`

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

# `get_is_autocommit`

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_last_insert_rowid`

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_max_write_frame`

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 primary

## Notes
  - Returns 0 if the database doesn't track write replication index
  - Different from `get_frame_number_for_replica/1` which 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_stmt_columns`

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_total_changes`

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`

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_ext`

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)

# `max_write_replication_index`

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/1` for 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_arguments`

```elixir
@spec normalise_arguments(String.t(), String.t(), list() | map()) ::
  list() | {:error, term()}
```

Normalise query arguments to a positional parameter list.

## Arguments

- `conn_id` - The connection identifier
- `statement` - 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"}`.

# `param_cache_size`

```elixir
@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`

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_stmt`

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_savepoint_by_name`

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_update_hook`

Remove the update hook from a connection.

**NOT SUPPORTED** - Update hooks are not currently implemented.

## Returns
  - `:unsupported` - Always returns unsupported

# `reset`

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_stmt`

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)

# `rollback`

```elixir
@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_savepoint_by_name`

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)

# `stmt_column_count`

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

# `stmt_column_name`

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"

# `stmt_parameter_count`

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

# `stmt_parameter_name`

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., `:id` returns `"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 = nil

## Notes
  - 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 `nil` for positional `?` placeholders

# `sync`

```elixir
@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_replica` mode)

## 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_replica` mode connections
- For `:local` mode, this is a no-op
- For `:remote` mode, data is already on the remote server
- Sync happens synchronously and may take time depending on data size

# `sync_until_frame`

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-date

## Notes
  - 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

# `vector`

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 (?)"

# `vector_distance_cos`

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"

# `vector_type`

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})"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
