A composable ORM toolkit for March.
Depot covers the full lifecycle of working with relational data: defining schemas, building queries, validating input, running migrations, executing against PostgreSQL, and inspecting query plans. It ships its own PostgreSQL wire-protocol client, so it has no external driver dependency.
let adults =
Depot.Query.from(user_schema())
|> Depot.Query.where_gt("age", "18")
|> Depot.Query.order_asc("name")
|> Depot.Query.limit(20)
|> Depot.Query.exec_sql(conn)
- Composable queries —
Depot.Querybuilds a pure value describing a query; execution (in-memory or SQL) is a separate step. - Changeset-style validation —
Depot.Gatecasts and validates input and collects all errors (non-short-circuiting) so users see a complete list. - Schema-driven — define tables, field types, and associations once.
- Migrations — generate and run versioned
up/downmigrations via theforge depotCLI. - Built-in PostgreSQL client — connection, SCRAM/MD5 auth, prepared statements, and a connection pool, implemented over the wire protocol.
- Transactions —
Depot.Transactionwith automatic savepoint nesting. - Test-friendly —
Depot.Repois an in-memory store (backed by Vault), so unit tests need no database. - Introspection —
Depot.ExplainforEXPLAIN/EXPLAIN ANALYZE, andDepot.Telemetryfor query events.
Depot is a March library. Add it to your project's forge.toml:
[deps]
depot = { git = "https://github.com/march-language/depot.git" }Or pin a local checkout during development:
[deps]
depot = { path = "../depot" }Then import the modules you need:
import Depot.Schema
import Depot.Query
import Depot.Gate
import Connection
fn user_schema() do
Depot.Schema.define("users", {
fields = {
name = "String",
email = "String",
age = ("Int", { default = 0 })
}
})
end
Depot.Gate casts raw params and runs validations, accumulating every error:
fn user_gate(params) do
Depot.Gate.cast(params, ["name", "email", "age"])
|> Depot.Gate.validate_required(["name", "email"])
|> Depot.Gate.validate_format("email", "@")
|> Depot.Gate.validate_number("age", [NumMin(13)])
end
Against the in-memory repo (great for tests):
match Depot.Repo.insert(user_schema(), user_gate(params)) do
Ok(record) -> record
Err(gate) -> Depot.Gate.errors(gate)
end
let rows =
Depot.Query.from(user_schema())
|> Depot.Query.where_gt("age", "18")
|> Depot.Query.order_asc("name")
|> Depot.Query.limit(20)
|> Depot.Query.exec_sql(conn)
The same query value runs in memory via Depot.Query.execute(q, rows) — no
database required.
Connection provides a wire-protocol client (no external driver needed):
import Connection
match connect(default_config("postgres", "my_db")) do
Err(e) -> handle(e)
Ok(conn) ->
let _ = simple_query(conn, "CREATE TABLE IF NOT EXISTS users (id INT)")
let result = exec_prepared(conn, "SELECT id FROM users WHERE id = $1", [ParamText("1")])
close(conn)
end
default_config(user, database) defaults to 127.0.0.1:5432 with no password
(set password for SCRAM-SHA-256 / MD5 auth). For concurrent workloads, use the
connection Pool. Wrap multi-statement work in Depot.Transaction.run for
automatic BEGIN/COMMIT with savepoint nesting.
| Module | Responsibility |
|---|---|
Depot.Schema |
Define table schemas — field names, types, associations |
Depot.Query |
Composable query builder (in-memory execute / exec_sql) |
Depot.Repo |
In-memory repository for testing (insert/get/all/delete) |
Depot.Gate |
Changeset-style casting + validation |
Depot.Migration |
DDL generation with up/down versioning |
Depot.Transaction |
Transactions with automatic savepoint nesting |
Depot.Type |
Custom type adapters (Email, Enum) for field coercion |
Depot.Embed |
Embedded schemas — nested records inside a row |
Depot.Explain |
EXPLAIN / EXPLAIN ANALYZE query-plan inspection |
Depot.Telemetry |
Query-event instrumentation |
Connection / Pool |
PostgreSQL wire-protocol client and connection pool |
Use forge search "<name>" to explore functions, types, and docstrings.
Depot's forge depot tasks manage versioned migrations. Migration files live in
priv/depot/migrations/ and applied versions are tracked in
.march/depot/migrations.log.
| Command | Description |
|---|---|
forge depot gen.migration <name> |
Scaffold a new timestamped migration file |
forge depot migrate |
Run all pending migrations (up) |
forge depot rollback [n] |
Roll back the last n migrations (default 1) |
forge depot migrations |
List migrations and their applied status |
forge depot reset |
Roll back every applied migration |
A generated migration is a module with up, down, and version functions:
-- priv/depot/migrations/<timestamp>_create_users.march
mod Migrations.CreateUsers do
fn up() do
Depot.Migration.create_table("users", {
id = ("UUID", { primary_key = true }),
name = ("String", { null = false }),
email = ("String", { null = false })
})
end
fn down() do
Depot.Migration.drop_table("users")
end
fn version() do <timestamp> end
end
lib/
├── depot.march # entry point / facade
├── sql/ # query engine: ast, build, compile, cursor, schema
├── mutation/ # write path: mutation(+_build/_compile/_exec)
├── wire/ # postgres wire protocol: message, encode, decode, connection, pool
├── data/ # data shaping: depot_schema, depot_type, depot_embed, depot_gate
├── api/ # public Depot.* API: query, repo, migration, transaction, …
└── forge/ # forge CLI tasks (cmd_depot)
forge check # fast typecheck
forge build # compile
forge lint --strict # lint
forge test # run the test suiteThe full suite runs through the interpreter:
MARCH_TEST_INTERPRETER=1 forge testLive PostgreSQL tests (test/*_live.march) require a local server with a
depot_test database that trusts local connections; they error at
Connection.connect when the fixture is missing.