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), orNonewhen theOnConflictpolicy isNothingand a conflicting row already existed. The fourth argument is the columns to return. - insert_all
-
Insert many rows into
sourcein one statement. Every row sharescolumns(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 aNothingon-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
bodyinside a transaction. OnError, the adapter rolls back.bodyreceives a transaction-scopedAdapterwhose operations run on the transaction’s connection. Callers must use that adapter (the repo wraps it in a scopedRepo) so the work actually happens inside the transaction rather than on a separate pooled connection.The payload is
Dynamicsorepo.transactioncan 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 migrationup/downSQL). Used by the migrator. - query_sql
-
Execute a raw SELECT with positional parameters (
$1,$2, …) and return the rows. Portability caveat: SQLite parses$Nas a named parameter indexed by order of first occurrence in the SQL, and the argument list is bound positionally — the numeral is ignored. So$Nis only portable when the placeholders appear in ascending order ($1before$2, …) with no repeats; out-of-order or repeated$Nsilently binds different values on SQLite than on Postgres. Used by the migrator to readschema_migrations, by codegen introspection, and exposed publicly viarepo.query_raw. Pass[]for a parameterless statement. (Not supported by the in-memory adapter.) - stream
-
Stream the rows matching
querythroughreducerin batches of the given size, folding an accumulator without materializing the whole result set (backsrepo.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(liketransaction) so the repo can stay generic over the caller’s type.reduceris applied to each row in order and may returnError(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
Dynamicbecause 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
pogconnection (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)