What Is an Ecto-Style ORM?
This group builds a miniature ORM in the style of Ecto, the database library of the Elixir world. Most readers here have never written Elixir, so this page explains what that style is and why it is worth copying, before the following chapters build it in Zig.
The idea in one sentence
Ecto splits database work into pieces that are plain data and pure functions, and funnels every actual database interaction through one explicit gateway called the Repo.
What that looks like
In Ecto, an application defines a schema, builds queries as values, and hands them to the Repo:
# Elixir (Ecto), for flavor only
defmodule User do
schema "users" do
field :name, :string
field :age, :integer
end
end
query = from u in User, where: u.age > 18
adults = Repo.all(query)
Three separable ideas are hiding in those lines:
- The schema is a declaration, not a class with behavior. It states the shape of a row and nothing else.
- A query is a value. Building it runs no SQL; it can be composed, passed around, and inspected. Only handing it to the Repo touches the database.
- The Repo is the only gateway. There is no
user.save(); records do not know how to persist themselves. One object owns the connection, and everything goes through it.
Why this maps so well to Zig
Each of those ideas lands on a Zig feature almost exactly:
| Ecto idea | Zig mechanism |
|---|---|
| schema as declaration | a plain struct, read by @typeInfo at compile time |
| query as a value | a builder struct; chaining is just functions returning it |
| Repo as the gateway | Repo(Adapter), a type function with one stateful field |
| validations on the schema | a rules declaration, discovered with @hasDecl |
Zig even improves on the original in one respect: what Ecto checks when the query runs, Zig can check when the query compiles. A misspelled field in a filter is a compile error in the query builder chapter, not a runtime database error.
What the alternative looks like
The other famous ORM shape is Active Record (Rails, Django): the record
is an object that saves itself, user.save() and User.find(1).
Convenient at first, it couples every model to a global connection,
makes tests need a database, and hides queries behind attribute access.
The Ecto shape has more visible plumbing, and in exchange every piece is
testable alone, which is exactly the property the
Repo chapter exploits with its
recording driver.
Where each idea lives in this group
- Schema as declaration: A Type Function as the Front Door
- Query as a value: A Typed Query Builder
- Declared validations: Validation from Declarations
- Deriving writes from the schema: Writing Rows
- One gateway, faked in tests: The Repo
- Bracketing user code safely: Transactions and errdefer
The Elixir snippet above is the only unverified code in this group; it is here as a reference point, not as something to run. Every Zig snippet that follows is compiled and executed by CI.