defmodule ExTurso do
@moduledoc """
A thin Elixir wrapper around the [`turso`](https://crates.io/crates/turso)
Rust crate, exposed as a `DBConnection` pool over Rustler NIFs.
## Starting a pool
`ExTurso` is startable under a supervision tree:
children = [
{ExTurso, database: "my_app.db", name: MyApp.DB}
]
Supervisor.start_link(children, strategy: :one_for_one)
Then run queries against the registered name:
{:ok, _} = ExTurso.execute(MyApp.DB, "CREATE TABLE users (id INTEGER, name TEXT)")
{:ok, _} = ExTurso.execute(MyApp.DB, "INSERT INTO users VALUES (?, ?)", [1, "Alice"])
{:ok, %ExTurso.Result{rows: [%{"name" => "Alice"}]}} =
ExTurso.query(MyApp.DB, "SELECT name FROM users WHERE id = ?", [1])
## Turso Cloud sync
Pass `:remote_url` and `:auth_token` to open the local file as an embedded
replica of a Turso Cloud database, then call `sync/2` to synchronize:
children = [
{ExTurso,
database: "replica.db",
remote_url: "libsql://my-db.turso.io",
auth_token: fn -> System.fetch_env!("TURSO_AUTH_TOKEN") end,
name: MyApp.DB}
]
:ok = ExTurso.sync(MyApp.DB)
## Options
All options are forwarded to `DBConnection.start_link/2`. The
`ExTurso`-specific options are:
* `:database` — path to the local database file (required); `":memory:"`
opens an in-memory database per pooled connection
* `:remote_url` — URL of a Turso Cloud database to sync with (requires
`:auth_token`)
* `:auth_token` — auth token for the remote database, either a string or
a zero-arity function returning one; prefer the function form so the
token does not sit in supervisor child specs and crash reports
"""
alias ExTurso.Query
@type conn :: DBConnection.conn()
@doc """
Returns a child specification so `ExTurso` can be supervised directly:
{ExTurso, database: "my_app.db", name: MyApp.DB}
"""
@spec child_spec(keyword()) :: Supervisor.child_spec()
def child_spec(opts) do
DBConnection.child_spec(ExTurso.Connection, opts)
end
@doc """
Starts a connection pool linked to the current process.
Accepts `:database` plus any `DBConnection.start_link/2` option (e.g. `:name`,
`:pool_size`).
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
DBConnection.start_link(ExTurso.Connection, opts)
end
@doc """
Runs a query and returns `{:ok, %ExTurso.Result{}}` or `{:error, exception}`.
Rows come back as a list of maps keyed by column name.
"""
@spec query(conn(), String.t(), list(), keyword()) ::
{:ok, ExTurso.Result.t()} | {:error, Exception.t()}
def query(conn, statement, params \\ [], opts \\ []) do
run(conn, statement, :query, params, opts)
end
@doc """
Executes a statement and returns `{:ok, %ExTurso.Result{}}` whose `num_rows`
is the affected-row count, or `{:error, exception}`.
"""
@spec execute(conn(), String.t(), list(), keyword()) ::
{:ok, ExTurso.Result.t()} | {:error, Exception.t()}
def execute(conn, statement, params \\ [], opts \\ []) do
run(conn, statement, :execute, params, opts)
end
@doc """
Triggers a synchronization of the local replica database with the remote Turso Cloud database.
"""
@spec sync(conn(), keyword()) :: :ok | {:error, Exception.t()}
def sync(conn, opts \\ []) do
query = %Query{statement: "SYNC", command: :sync}
case DBConnection.execute(conn, query, [], opts) do
{:ok, _query, _result} -> :ok
{:error, _exception} = error -> error
end
end
defp run(conn, statement, command, params, opts) do
query = %Query{statement: statement, command: command}
case DBConnection.execute(conn, query, params, opts) do
{:ok, _query, result} -> {:ok, result}
{:error, _exception} = error -> error
end
end
end