Repositories
Kraken executes every read, write, preload, and transaction through one Repo
effect. A production handler captures its PostgreSQL connection, so ordinary
application functions do not accept or retrieve a pool:
pub fun find_user : Int -> Result (Maybe User) Db.DbError needs {Db.Repo}
find_user id = Db.one (user_query id)Create and install the handler at the application boundary:
let connection = SagaPgo.connect config
let repo = Db.postgres_repo connection
app () with repoKeep this installation outside leaf query and service functions. That keeps the repository replaceable in tests and makes each function's database dependency visible in its effect row.
Query logging
Use the repository builder when the application wants structured PostgreSQL execution events:
fun make_repo : SagaPgo.Connection -> Handler Db.Repo needs {AppLog}
make_repo connection =
Db.repo_config connection
|> Db.log_queries Db.Queries (fun event -> debug! (show event))
|> Db.build_repoThe sink is effect-transparent: AppLog above belongs to the application, not
Kraken. It can instead write JSON, emit an OpenTelemetry span, append to a test
collector, or be a pure callback. Install the resulting repository exactly like
Db.postgres_repo.
Db.Queries emits one QueryLog per physical SQL statement. Relation preloads
therefore produce their own Preload events rather than being hidden inside the
parent all or one call. Db.Errors emits only statements rejected by
PostgreSQL.
Each event contains:
kind: the originating repository operation, orPreloadsql: SQL with PostgreSQL placeholdersparam_count: the number of bound parametersduration_us: elapsed execution timeresult:Ok countor the originalQueryError
Bound parameter values are deliberately not logged: web application parameters
frequently contain credentials, tokens, and personal data. show event provides
a useful single-line default, while structured sinks can consume the fields
directly.
Multiple databases
For one database, use Db.Repo directly. For two or more semantic repositories,
mint nominal copies of the interface:
pub neweffect AuthRepo = Db.Repo
pub neweffect DataRepo = Db.RepoThe effects intentionally do not satisfy one another or Db.Repo. Functions call
the operation from the repository they require:
fun find_user : Int -> Result (Maybe User) Db.DbError needs {AuthRepo}
find_user id = AuthRepo.one! (user_query id)
fun recent_activity : Int -> Result (List Activity) Db.DbError needs {DataRepo}
recent_activity id = DataRepo.all! (activity_query id)
fun dashboard : Int -> Result Page AppError needs {AuthRepo, DataRepo}Nominal adapter
An application defines the exact boundary between each nominal effect and a
concrete Handler Db.Repo. The separate transaction-body handler reinstalls the
nominal repository inside a transaction and rejects a nested transaction before it
can disturb the outer one:
import Kraken.Db
import SagaPgo as Pgo
fun auth_transaction_body : Handler Db.Repo -> Handler AuthRepo
auth_transaction_body backend = handler for AuthRepo {
run query = resume (Db.Repo.run! query with backend)
all query = resume (Db.Repo.all! query with backend)
one query = resume (Db.Repo.one! query with backend)
exactly_one query = resume (Db.Repo.exactly_one! query with backend)
exec statement = resume (Db.Repo.exec! statement with backend)
transaction _body =
resume Err (Pgo.TransactionFailed Pgo.NestedTransaction)
}
fun auth_repo : Handler Db.Repo -> Handler AuthRepo
auth_repo backend = handler for AuthRepo {
run query = resume (Db.Repo.run! query with backend)
all query = resume (Db.Repo.all! query with backend)
one query = resume (Db.Repo.one! query with backend)
exactly_one query = resume (Db.Repo.exactly_one! query with backend)
exec statement = resume (Db.Repo.exec! statement with backend)
transaction body = {
let body_repo = auth_transaction_body backend
resume (Db.Repo.transaction! (fun () -> body () with body_repo) with backend)
}
}Build one independently captured backend per database:
let auth = auth_repo (Db.postgres_repo auth_connection)
let data = data_repo (Db.postgres_repo data_connection)
dashboard user_id with {auth, data}If two connections target the same PostgreSQL database, give their saga_pgo
configs distinct pool_name values before calling connect.
A route may require both effects, but a transaction belongs to exactly one repository. Kraken does not imply a distributed transaction across two databases.
Transactions
The base repository wrapper is connectionless:
Db.transaction (fun () -> {
case Db.exec (insert_user user) {
Err err -> Err err
Ok _ -> Db.exec (insert_profile profile)
}
})Ok value commits. Returning Err error rolls back and produces
Err (RolledBack error). Inside the body, rollback! error exits immediately,
skips the continuation, and produces the same result. A nested transaction fails
with TransactionFailed NestedTransaction; savepoints are not yet supported.
Nominal repositories use their copied operation directly:
AuthRepo.transaction! (fun () -> {
# AuthRepo.one!, AuthRepo.exec!, ...
})Test backends
Db.empty_repo is deterministic and does not inspect SQL:
allreturnsOk []onereturnsOk Nothingexactly_onereturnsErr (ExpectedOneRow 0)execreturnsOk 0- transactions execute their body with normal returned-error and
rollback!behavior
Db.rejecting_repo message lets transaction callbacks run but panics at the first
read or write. Use it to prove validation or authorization exits before touching
the database.
For integration tests, use Db.postgres_repo test_connection through the same
base or nominal adapter used in production. Kraken intentionally does not pretend
to be an in-memory SQL engine. A savepoint-based sandbox is future work.