Skip to main content

lib/dllb.ex

defmodule Dllb do
  @moduledoc """
  Elixir client for the dllb multi-model NoSQL database.

  Provides a high-level API that delegates to a NimblePool-managed
  connection pool. Configure the pool in your application config:

      config :dllb,
        enabled: true,
        host: "127.0.0.1",
        port: 3009,
        pool_size: 5,
        outcome: :json,
        timeout: 30_000

  ## Usage

      {:ok, result} = Dllb.query("SELECT * FROM users")
      result = Dllb.query!("SELECT * FROM users")
  """

  @doc """
  Executes a query through the connection pool.

  Returns `{:ok, result}` or `{:error, reason}`.
  """
  @spec query(String.t()) :: {:ok, Dllb.Result.t()} | {:error, term()}
  def query(query_string), do: Dllb.Pool.query(query_string)

  @doc """
  Executes a query through the connection pool, raising on error.

  Returns the result struct on success or raises `Dllb.Error`.
  """
  @spec query!(String.t()) :: Dllb.Result.t()
  def query!(query_string) do
    case Dllb.Pool.query(query_string) do
      {:ok, %Dllb.Result.Error{message: message}} ->
        raise Dllb.Error, %{message: message, type: :query_error}

      {:ok, result} ->
        result

      {:error, reason} ->
        raise Dllb.Error, %{message: "query failed: #{inspect(reason)}", type: :connection_error}
    end
  end

  @doc """
  Executes multiple queries in a single pool checkout.

  Returns a list of `{:ok, result} | {:error, reason}` in the same
  order as the input queries. This is significantly faster for bulk
  operations (e.g. AST ingestion) because it amortises the pool
  checkout overhead.
  """
  @spec batch([String.t()]) :: [{:ok, Dllb.Result.t()} | {:error, term()}]
  def batch(query_strings) when is_list(query_strings) do
    Dllb.Pool.batch(query_strings)
  end

  @doc """
  Executes multiple queries inside a `BEGIN BATCH ... END BATCH` block.

  All statements run in a single server-side storage transaction,
  eliminating per-statement write-commit overhead. Returns a single
  result (not a list).

  This is dramatically faster than `batch/1` for bulk writes because
  the server commits only once instead of once per statement.
  """
  @spec batch_transaction([String.t()]) :: {:ok, Dllb.Result.t()} | {:error, term()}
  def batch_transaction(query_strings) when is_list(query_strings) do
    Dllb.Pool.batch_transaction(query_strings)
  end

  @doc """
  Like `batch/1` but raises on the first error encountered.

  Returns a list of result structs on success.
  """
  @spec batch!([String.t()]) :: [Dllb.Result.t()]
  def batch!(query_strings) when is_list(query_strings) do
    query_strings
    |> Dllb.Pool.batch()
    |> Enum.map(fn
      {:ok, %Dllb.Result.Error{message: message}} ->
        raise Dllb.Error, %{message: message, type: :query_error}

      {:ok, result} ->
        result

      {:error, reason} ->
        raise Dllb.Error, %{
          message: "batch query failed: #{inspect(reason)}",
          type: :connection_error
        }
    end)
  end
end