Architecture
View SourceThis document describes the internal architecture of Kura and how the modules fit together.
Module Overview
┌─────────────────────────────────────────────────┐
│ User Code │
│ (YourRepo, YourSchema modules) │
└──────────┬──────────────────────┬───────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ kura_repo │ │ kura_schema │
│ (behaviour) │ │ (behaviour) │
└──────┬──────┘ └──────┬──────┘
│ │
┌──────▼──────────────────────▼───────────────┐
│ kura_repo_worker │
│ (query execution, type conversion, txns) │
└──────┬──────────────┬───────────────────────┘
│ │
┌──────▼──────┐ ┌─────▼──────────┐
│ kura_query │ │ kura_changeset │
│ (builder) │ │ (validation) │
└──────┬──────┘ └────────────────┘
│
┌──────▼──────────────┐
│ kura_query_compiler │
│ (SQL generation) │
└──────┬──────────────┘
│
┌──────▼──────┐
│ pgo │
│ (PG driver) │
└─────────────┘Core Modules
kura_schema
Behaviour that defines the shape of a database table. Schemas declare
their table name, fields (via #kura_field{} records), primary key,
and optionally associations and embeds. Field metadata is cached in
persistent_term for fast lookups at runtime.
kura_changeset
Tracks changes between a schema's current data and incoming params.
Provides casting, validations (validate_required, validate_format,
validate_length, etc.), and constraint declarations that map PostgreSQL
constraint errors back to user-facing field errors.
kura_changeset_assoc
Handles nested association and embed casting (cast_assoc/2,3,
cast_embed/2,3, put_assoc/3). Builds child changesets from nested
params and manages the assoc_changes list on the parent changeset.
kura_query
Composable, functional query builder. Queries are built by chaining
calls like from/1, where/2, order_by/2, limit/2, and
preload/2. The query record accumulates conditions without executing
anything — execution happens in kura_repo_worker.
kura_query_compiler
Translates a #kura_query{} record into parameterized SQL ($1, $2,
etc.) suitable for pgo. Handles SELECT, INSERT, UPDATE, DELETE, and
bulk operations. Supports fragments for raw SQL escape hatches.
kura_repo and kura_repo_worker
kura_repo is a thin behaviour — user repo modules implement otp_app/0
and delegate operations to kura_repo_worker. Database configuration is
read from application environment via application:get_env(OtpApp, RepoModule).
The worker handles:
- Query execution via
pgo - Type conversion between Erlang terms and PostgreSQL values (dump/load)
- PostgreSQL error mapping (constraint violations → changeset errors)
- Transaction wrapping for changesets with
assoc_changes - Query telemetry (optional logging of queries, durations, results)
kura_preloader
Executes preload queries after the primary query completes. Uses
WHERE ... IN (...) queries rather than JOINs, supporting nested
preloads and all association types including many-to-many via join tables.
kura_multi
Transaction pipelines inspired by Ecto.Multi. Lets you compose named
steps (insert, update, delete, run) where each step can access
the accumulated results of previous steps. The entire pipeline executes
inside a single database transaction.
kura_types
Type system that defines how Erlang values map to PostgreSQL column types.
Each type implements cast (user input → internal), dump (internal →
PG), and load (PG → internal). Supported types include id, integer,
float, string, text, boolean, date, utc_datetime, uuid,
jsonb, {enum, [atom()]}, {array, T}, and {embed, Type, Module}.
kura_migration and kura_migrator
kura_migration is a behaviour for writing migrations with up/0 and
down/0 callbacks. kura_migrator discovers migration modules,
maintains a schema_migrations table, and applies pending migrations
in order.
Data Flow
A typical insert operation flows through the system like this:
- User calls
MyRepo:insert(Changeset) kura_repo_worker:insert/2validates the changeset- If
assoc_changesis non-empty, wraps everything in a transaction kura_query_compilergeneratesINSERT INTO ... RETURNING *kura_typesdumps each changed field to its PG representationpgoexecutes the querykura_typesloads the returned row back into Erlang terms- If there are association changes, child records are persisted with the parent's PK set as their foreign key
- On PG constraint errors, the error is mapped back to a changeset error via declared constraints