Database layer for Erlang — Ecto-equivalent abstractions in pure Erlang, targeting PostgreSQL via pgo.
- Schema — behaviour-based schema definitions with type metadata
- Changeset — cast external params, validate, track changes and errors
- Query Builder — composable, functional query construction
- SQL Compiler — parameterized SQL generation (no string interpolation)
- Repo — CRUD operations with automatic type conversion and PG error mapping
- Associations —
belongs_to,has_one,has_many,many_to_manywith preloading - Embedded Schemas —
embeds_one,embeds_manystored as JSONB - Multi — atomic transaction pipelines
- Migrations — DDL operations with automatic module-based discovery
- Enums — atom-backed enum types stored as
VARCHAR - Telemetry — query logging with timing
-module(user).
-behaviour(kura_schema).
-include_lib("kura/include/kura.hrl").
-export([table/0, fields/0]).
table() -> <<"users">>.
fields() ->
[
#kura_field{name = id, type = id, primary_key = true, nullable = false},
#kura_field{name = name, type = string, nullable = false},
#kura_field{name = email, type = string, nullable = false},
#kura_field{name = age, type = integer},
#kura_field{name = inserted_at, type = utc_datetime},
#kura_field{name = updated_at, type = utc_datetime}
].-module(my_repo).
-behaviour(kura_repo).
-export([otp_app/0, start/0, all/1, get/2, insert/1, update/1, delete/1]).
otp_app() -> my_app.
start() -> kura_repo_worker:start(?MODULE).
all(Q) -> kura_repo_worker:all(?MODULE, Q).
get(Schema, Id) -> kura_repo_worker:get(?MODULE, Schema, Id).
insert(CS) -> kura_repo_worker:insert(?MODULE, CS).
update(CS) -> kura_repo_worker:update(?MODULE, CS).
delete(CS) -> kura_repo_worker:delete(?MODULE, CS).Configure the database connection in sys.config:
[{my_app, [
{my_repo, #{
database => <<"myapp">>,
hostname => <<"localhost">>,
port => 5432,
username => <<"postgres">>,
password => <<>>,
pool_size => 10
}}
]}].%% Cast and validate external params
CS = kura_changeset:cast(user, #{}, #{<<"name">> => <<"Alice">>, <<"email">> => <<"alice@example.com">>}, [name, email, age]),
CS1 = kura_changeset:validate_required(CS, [name, email]),
CS2 = kura_changeset:validate_format(CS1, email, <<"@">>),
CS3 = kura_changeset:validate_length(CS2, name, [{min, 1}, {max, 100}]),
%% Insert
{ok, User} = my_repo:insert(CS3).Q = kura_query:from(user),
Q1 = kura_query:where(Q, {age, '>', 18}),
Q2 = kura_query:where(Q1, {'or', [{role, <<"admin">>}, {role, <<"moderator">>}]}),
Q3 = kura_query:select(Q2, [name, email]),
Q4 = kura_query:order_by(Q3, [{name, asc}]),
Q5 = kura_query:limit(Q4, 10),
{ok, Users} = my_repo:all(Q5).Supported conditions: =, !=, <, >, <=, >=, like, ilike, in, not_in, is_nil, is_not_nil, between, {'and', [...]}, {'or', [...]}, {'not', ...}, {fragment, SQL, Params}.
-module(m20240115120000_create_users).
-behaviour(kura_migration).
-include_lib("kura/include/kura.hrl").
-export([up/0, down/0]).
up() ->
[{create_table, <<"users">>, [
#kura_column{name = id, type = id, primary_key = true, nullable = false},
#kura_column{name = name, type = string, nullable = false},
#kura_column{name = email, type = string, nullable = false},
#kura_column{name = age, type = integer},
#kura_column{name = inserted_at, type = utc_datetime},
#kura_column{name = updated_at, type = utc_datetime}
]},
{create_index, <<"users">>, [email], #{unique => true}}].
down() ->
[{drop_index, <<"users_email_index">>},
{drop_table, <<"users">>}].Run migrations:
kura_migrator:migrate(my_repo).
kura_migrator:rollback(my_repo).
kura_migrator:status(my_repo).| Kura | PostgreSQL | Erlang |
|---|---|---|
id |
BIGSERIAL |
integer() |
integer |
INTEGER |
integer() |
float |
DOUBLE PRECISION |
float() |
string |
VARCHAR(255) |
binary() |
text |
TEXT |
binary() |
boolean |
BOOLEAN |
boolean() |
date |
DATE |
{Y, M, D} |
utc_datetime |
TIMESTAMPTZ |
calendar:datetime() |
uuid |
UUID |
binary() |
jsonb |
JSONB |
map() |
{array, T} |
T[] |
list() |
Database config is read from application environment using application:get_env(OtpApp, RepoModule):
%% sys.config
[{my_app, [
{my_repo, #{
database => <<"my_app_dev">>,
hostname => <<"localhost">>,
port => 5432,
username => <<"postgres">>,
password => <<"postgres">>,
pool_size => 10
}}
]}].Migrations are discovered automatically from compiled modules implementing the kura_migration behaviour.
Optional telemetry/logging config:
[{kura, [
{log, true} %% true | {M, F} | false (default)
]}].- rebar3_kura — Rebar3 plugin that auto-generates migration files from schema changes. Add a field to your schema, run
rebar3 compile, and the migration is created for you. - opentelemetry_kura — OpenTelemetry instrumentation. Subscribes to Kura's telemetry events and creates spans for every database query.
- pet_store — A sample REST API built with Kura and Nova demonstrating schemas, changesets, queries, migrations, and associations in practice.
- Erlang/OTP 28+
- PostgreSQL 14+
