Depot
- Depot — database library for March.
-
- Depot is a composable ORM toolkit for March applications. It covers the
- full lifecycle of working with relational data: defining schemas, building
- queries, validating input, running migrations, and inspecting query plans.
-
- ## Sub-modules
-
| - | Module | Responsibility |
|---|---|---|
| - | Depot.Schema | Define table schemas — field names, types, and associations |
| - | Depot.Query | Composable query builder (from / where_eq / exec_sql) |
| - | Depot.Repo | In-memory repository for testing (insert/get/all/delete) |
| - | Depot.Gate | Changeset-style validation and casting (cast + validate) |
| - | Depot.Migration | Schema migrations — DDL generation with up/down versioning |
| - | Depot.Transaction | Database transactions with automatic savepoint nesting |
| - | Depot.Type | Custom type adapters (Email, Enum) for field coercion |
| - | Depot.Embed | Embedded schemas — nested assoc-list records inside a row |
| - | Depot.Explain | EXPLAIN / EXPLAIN ANALYZE query-plan inspection |
| - | Depot.Url | Parse database URLs (postgres://, sqlite:) into targets |
| - | Depot.DbRepo | Named repo registry — start, migrate, query, transaction |
-
- ## Quick start
-
- ### 1. Define a schema
-
- fn user_schema() do
- Depot.Schema.define("users", {
- fields = {
- name = "String",
- email = "String",
- age = ("Int", { default: 0 })
- }
- })
- end
-
- ### 2. Validate input with Gate
-
- 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
-
- ### 3. Persist with Repo
-
- let gate = user_gate(conn_params)
- match Depot.Repo.insert(user_schema(), gate) do
- Ok(record) -> record
- Err(gate) -> Depot.Gate.errors(gate)
- end
-
- ### 4. Query
-
- 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(db_conn)
-
- ### 5. Named repos with Depot.DbRepo (Ecto-style)
-
- Start a repo from a URL (or an environment variable) once at app init,
- then use it by name everywhere — no connection handle to thread around.
-
- -- In your app's init or main:
- let _ = Depot.DbRepo.start_from_env("MyApp.Repo", "DATABASE_URL",
- "sqlite::memory:")
- let _ = Depot.DbRepo.migrate("MyApp.Repo", MyApp.Migrations.all())
-
- -- Anywhere in your app:
- match Depot.DbRepo.query("MyApp.Repo", my_query) do
- Ok(rows) -> rows
- Err(e) -> ...
- end
-
- match Depot.DbRepo.transaction("MyApp.Repo", fn conn ->
- Db.query(conn, "INSERT INTO ...")
- ) do
- Ok(_) -> ...
- Err(e) -> ...
- end
-
- -- In tests (SQLite in-memory, no server required):
- let _ = Depot.DbRepo.start("test_repo", "sqlite::memory:")
- let _ = Depot.DbRepo.migrate("test_repo", MyApp.Migrations.all())
-
- ## Design principles
-
- - No magic: every operation is an explicit function call on a plain value.
- - Non-short-circuiting validation: Gate collects all errors before you
- inspect them, so users see a complete error list on first submit.
- - Test-friendly: Depot.Repo is an in-memory store backed by Vault;
- no database required for unit tests.
- - Composable queries: Depot.Query builds a pure value describing the
- query. Execution (in-memory vs SQL) is a separate step.