lode/adapter

The adapter interface: what a database backend must provide.

Ecto adapters are Elixir behaviours (Ecto.Adapter, .Queryable, .Schema, .Transaction). Gleam has no behaviours, so an adapter is a record of functions — a value the repo holds and calls. The in-memory adapter (lode/adapters/memory), the pog-based Postgres adapter, and the sqlight-based SQLite adapter all produce an Adapter.

Rows cross this boundary as Dict(String, Value) (DB-canonical). The repo layer turns rows into typed structs via the schema’s load.

Types

pub type Adapter {
  Adapter(
    all: fn(query.Query) -> Result(
      List(dict.Dict(String, value.Value)),
      error.LodeError,
    ),
    insert: fn(
      String,
      option.Option(String),
      dict.Dict(String, value.Value),
      List(String),
      on_conflict.OnConflict(Nil),
    ) -> Result(
      option.Option(dict.Dict(String, value.Value)),
      error.LodeError,
    ),
    insert_all: fn(
      String,
      List(String),
      List(List(value.Value)),
      on_conflict.OnConflict(Nil),
    ) -> Result(
      List(dict.Dict(String, value.Value)),
      error.LodeError,
    ),
    update_all: fn(query.Query, List(query.Assignment)) -> Result(
      Int,
      error.LodeError,
    ),
    delete_all: fn(query.Query) -> Result(Int, error.LodeError),
    transaction: fn(
      fn(Adapter) -> Result(dynamic.Dynamic, error.LodeError),
    ) -> Result(dynamic.Dynamic, error.LodeError),
    execute_ddl: fn(String) -> Result(Nil, error.LodeError),
    query_sql: fn(String, List(value.Value)) -> Result(
      List(dict.Dict(String, value.Value)),
      error.LodeError,
    ),
    stream: fn(
      query.Query,
      Int,
      dynamic.Dynamic,
      fn(dynamic.Dynamic, dict.Dict(String, value.Value)) -> Result(
        dynamic.Dynamic,
        error.LodeError,
      ),
    ) -> Result(dynamic.Dynamic, error.LodeError),
    checkout: fn(fn(dynamic.Dynamic) -> dynamic.Dynamic) -> dynamic.Dynamic,
    engine: Engine,
  )
}

Constructors

  • Adapter(
      all: fn(query.Query) -> Result(
        List(dict.Dict(String, value.Value)),
        error.LodeError,
      ),
      insert: fn(
        String,
        option.Option(String),
        dict.Dict(String, value.Value),
        List(String),
        on_conflict.OnConflict(Nil),
      ) -> Result(
        option.Option(dict.Dict(String, value.Value)),
        error.LodeError,
      ),
      insert_all: fn(
        String,
        List(String),
        List(List(value.Value)),
        on_conflict.OnConflict(Nil),
      ) -> Result(
        List(dict.Dict(String, value.Value)),
        error.LodeError,
      ),
      update_all: fn(query.Query, List(query.Assignment)) -> Result(
        Int,
        error.LodeError,
      ),
      delete_all: fn(query.Query) -> Result(Int, error.LodeError),
      transaction: fn(
        fn(Adapter) -> Result(dynamic.Dynamic, error.LodeError),
      ) -> Result(dynamic.Dynamic, error.LodeError),
      execute_ddl: fn(String) -> Result(Nil, error.LodeError),
      query_sql: fn(String, List(value.Value)) -> Result(
        List(dict.Dict(String, value.Value)),
        error.LodeError,
      ),
      stream: fn(
        query.Query,
        Int,
        dynamic.Dynamic,
        fn(dynamic.Dynamic, dict.Dict(String, value.Value)) -> Result(
          dynamic.Dynamic,
          error.LodeError,
        ),
      ) -> Result(dynamic.Dynamic, error.LodeError),
      checkout: fn(fn(dynamic.Dynamic) -> dynamic.Dynamic) -> dynamic.Dynamic,
      engine: Engine,
    )

    Arguments

    all

    Run a SELECT, returning matching rows (full columns; the repo projects).

    insert

    Insert one row into source (optionally schema-qualified by the second argument, a Postgres schema prefix); return the stored row (with any generated keys filled in), or None when the OnConflict policy is Nothing and a conflicting row already existed. The fourth argument is the columns to return.

    insert_all

    Insert many rows into source in one statement. Every row shares columns (second argument); each inner list holds that row’s values in the same order. Returns the stored rows (with any generated keys filled in), in insertion order — minus any rows a Nothing on-conflict policy skipped. Callers guarantee at least one row.

    update_all

    Apply column sets (literal or expression-valued) to every row matching the query’s filters; return the number of affected rows.

    delete_all

    Delete every row matching the query’s filters; return the count.

    transaction

    Run body inside a transaction. On Error, the adapter rolls back.

    body receives a transaction-scoped Adapter whose operations run on the transaction’s connection. Callers must use that adapter (the repo wraps it in a scoped Repo) so the work actually happens inside the transaction rather than on a separate pooled connection.

    The payload is Dynamic so repo.transaction can stay generic over the caller’s return type; the adapter only passes it through or discards it.

    execute_ddl

    Execute a raw, non-returning statement (DDL like CREATE TABLE, or migration up/down SQL). Used by the migrator.

    query_sql

    Execute a raw SELECT with positional parameters ($1, $2, …) and return the rows. Portability caveat: SQLite parses $N as a named parameter indexed by order of first occurrence in the SQL, and the argument list is bound positionally — the numeral is ignored. So $N is only portable when the placeholders appear in ascending order ($1 before $2, …) with no repeats; out-of-order or repeated $N silently binds different values on SQLite than on Postgres. Used by the migrator to read schema_migrations, by codegen introspection, and exposed publicly via repo.query_raw. Pass [] for a parameterless statement. (Not supported by the in-memory adapter.)

    stream

    Stream the rows matching query through reducer in batches of the given size, folding an accumulator without materializing the whole result set (backs repo.stream_fold). The Postgres adapter drives a server-side cursor (DECLARE/FETCH/CLOSE) inside a transaction, so peak memory is one batch and the rows are one MVCC snapshot; the SQLite adapter steps a prepared statement row by row (embedded, so the batch size is irrelevant — peak memory is one row); the in-memory adapter folds its matched rows directly.

    The accumulator crosses as Dynamic (like transaction) so the repo can stay generic over the caller’s type. reducer is applied to each row in order and may return Error (e.g. a row that fails to load), which aborts the stream and rolls the transaction back.

    checkout

    Run the callback with exclusive access to the adapter’s underlying engine connection — the escape hatch for engine-specific code the value layer cannot express: compiled query functions (squirrel / marmot), pragmas, COPY. The connection crosses as Dynamic because this record is engine-agnostic; call it through the owning adapter module’s typed wrapper (sqlite.with_connection, postgres.with_connection), never directly.

    Exclusivity is the point: the SQLite adapter runs the callback holding its connection lock (and bare on a transaction-scoped adapter, whose outer acquire — reentrant per process — already covers it, so the callback’s statements join the open transaction); the Postgres adapter hands its pog connection (the transaction’s own connection on a transaction-scoped adapter). The in-memory adapter has no connection and panics.

    engine

    Which engine this adapter drives (selects the migrator’s DDL dialect).

Which database engine an adapter drives. The migrator reads this to pick the DDL dialect (the lode/query/sql query dialect is chosen inside each adapter already); Generic covers non-SQL adapters like the in-memory one.

pub type Engine {
  Postgres
  Sqlite
  Generic
}

Constructors

  • Postgres
  • Sqlite
  • Generic

A database row: column name -> canonical Value.

pub type Row =
  dict.Dict(String, value.Value)
Search Document