Kura

Kura

Database layer for Erlang — Ecto-equivalent abstractions in pure Erlang, targeting PostgreSQL via pgo.

Features

Quick Start

Define a Schema

-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}
    ].

Define a Repo

-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
    }}
]}].

Changesets

%% 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).

Query Builder

Q = kura_query:from(user),
Q1 = kura_query:where(Q, {age, &#39;>&#39;, 18}),
Q2 = kura_query:where(Q1, {&#39;or&#39;, [{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}.

Migrations

-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).

Type Mapping

Kura PostgreSQL Erlang
idBIGSERIALinteger()
integerINTEGERinteger()
floatDOUBLE PRECISIONfloat()
stringVARCHAR(255)binary()
textTEXTbinary()
booleanBOOLEANboolean()
dateDATE{Y, M, D}
utc_datetimeTIMESTAMPTZcalendar:datetime()
uuidUUIDbinary()
jsonbJSONBmap()
{array, T}T[]list()

Configuration

Configure the database connection under the kura application env. Kura starts the pgo pool automatically during application startup:

%% sys.config
[{kura, [
    {repo, my_repo},
    {host, "localhost"},
    {port, 5432},
    {database, "my_app_dev"},
    {user, "postgres"},
    {password, "postgres"},
    {pool_size, 10}     %% default: 10
]}].

Defaults: host = "localhost", port = 5432, user = "postgres", pool_size = 10.

UUID primary keys are auto-generated on insert when no value is provided.

Legacy per-app config (still supported) ```erlang [{my_app, [ {my_repo, #{ database => ~"my_app_dev", hostname => ~"localhost", port => 5432, username => ~"postgres", password => ~"postgres", pool_size => 10 }} ]}]. ``` With this style the consuming app must call `my_repo:start()` to create the pgo pool. The kura app env config is checked first; if `{kura, [{repo, _}]}` is not set, Kura falls back to this pattern.

Migrations are discovered automatically from compiled modules implementing the kura_migration behaviour.

Optional telemetry/logging config:

[{kura, [
    {log, true}  %% true | {M, F} | false (default)
]}].

Plugins

Examples

Requirements