Something went wrong. Try again.
a database layer insipred by caqti and ecto
Something went wrong. Try again.
OCaml 98%
Shell <1%
<1%
Dune <1%
Standard ML <1%
README.md
repodb #
An Ecto-inspired database toolkit for OCaml.
repodb tries to bring Elixir's Ecto ideas to OCaml: type-safe queries, composable changesets, and clean separation between your domain and the database.
Features #
- Schemas - Define your data structures with typed fields
- PPX bindings - Derive fields, schemas, and row codecs from OCaml records
- Changesets - Validate and track changes before persisting
- Query DSL - Build type-safe queries with a composable API
- Type-Safe INSERTs - Compile-time verification that column types match value types
- Repo - Clean abstraction over database operations
- Dialect-Aware DDL - Generate schema and migration SQL from the selected backend
- Shared Backend Contract Tests - Portable integration coverage runs against PostgreSQL, SQLite, and MariaDB
- Associations - Define relationships (has_many, belongs_to, many_to_many)
- Preloading - Efficiently load associations, avoiding N+1 queries
- Multi - Compose multiple operations in a single transaction
- Migrations - Version your database schema
- Connection Pool - Scheduler-agnostic, lock-free connection pooling
- CQRS - Transparent read/write splitting with replica support
- Multi-Server Pool - Round-robin load balancing across database servers
Quick Example #
(* Define a schema *)
let users_table = Schema.table "users"
type user = { id : int; name : string; email : string; age : int }
let name_field =
Field.make ~table_name:"users" ~name:"name" ~ty:Types.string
~get:(fun u -> u.name) ~set:(fun v u -> { u with name = v }) ()
let age_field =
Field.make ~table_name:"users" ~name:"age" ~ty:Types.int
~get:(fun u -> u.age) ~set:(fun v u -> { u with age = v }) ()
(* Create and validate a changeset *)
let empty_user = { id = 0; name = ""; email = ""; age = 0 }
let changeset =
Changeset.create empty_user
|> Changeset.cast [("name", "Alice"); ("email", "alice@example.com")]
~fields:[name_field; email_field]
|> Changeset.validate_required [name_field; email_field]
|> Changeset.validate_format email_field ~pattern:"^[^@]+@[^@]+$"
(* Build type-safe queries using field definitions *)
let query =
Query.(from users_table
|> where Expr.(column age_field > int 18)
|> order_by ~direction:Desc (Expr.column created_at_field)
|> limit 10)
let users = Repo.all_query conn query ~decode:decode_user
(* Type-safe insert - compiler verifies column/value types match *)
let insert_query =
Query.insert_into users_table
|> Query_values.values3
(name_field, email_field, age_field)
(Expr.string "Alice", Expr.string "alice@example.com", Expr.int 25)
Documentation #
- Changelog - Release notes and upgrade guidance
- Getting Started - Installation and basic setup
- Schemas - Defining tables and columns
- PPX bindings - Generate bindings from record definitions
- PPX examples - A runnable application with codecs, joins, updates, and migrations
- Changesets - Data validation and change tracking
- Queries - Building and executing queries
- Repo - Database operations
- Associations - Relationships and preloading
- Transactions - Multi and transaction handling
- Migrations - Database schema versioning
- Connection Pool - Scheduler-agnostic connection pooling
- CQRS - Read/write splitting with replicas
- Durable queues - Row locking, CTEs, and atomic claim queries
Packages #
| Package | Description |
|---|---|
repodb |
Core library with schemas, changesets, queries |
repodb-ppx |
Optional record deriver for fields, schemas, and row codecs |
repodb-sqlite |
SQLite driver |
repodb-postgresql |
PostgreSQL driver |
repodb-mariadb |
MariaDB/MySQL driver |
Installation #
opam install repodb repodb-sqlite # or repodb-postgresql / repodb-mariadb
MariaDB / MySQL #
repodb-mariadb supports the same core driver contract as the existing backends:
- connections and native transaction control (
Mariadb.Blocking.start_txn,commit,rollback) - no transaction-control statement is prepared or cached, so MySQL 8 never raises error 1295 and row locks are held until the transaction ends - prepared statements
- query execution and decoding, including full-width signed
BIGINTand unsignedBIGINTvalues, which are carried as exact decimal text aboveInt64.max_intinstead of silently wrapping - affected-row counts and full-width generated ids through
exec_result : ... -> (Driver.command_result, error) result - connection pools via
Pool.Make(Repodb_mariadb) - MySQL/MariaDB placeholder rendering with
? - MySQL/MariaDB upsert rendering with
ON DUPLICATE KEY UPDATE SELECT ... FOR UPDATE SKIP LOCKEDand common table expressions through the Query DSL
Connection strings use key=value pairs such as:
host=localhost port=3306 dbname=myapp user=root password=secret charset=utf8mb4
Runtime usage and backend-aware schema/migration SQL are supported now through
Repo.Ddl / Ddl.Make(Driver). Raw SQL migration types/defaults remain explicit
escape hatches, just like Ecto's adapters.
Current limitations:
RETURNINGflows are not available onrepodb-mariadb.UPDATE ... RETURNINGandUPDATE ... FROMare rejected at render time rather than emitted as SQL MySQL would refuse; seedocs/queues.mdfor the transactional two-step form.CREATE INDEX IF NOT EXISTSand the column guards are MariaDB-only. The sharedMySQLdialect defaults to the MySQL-safe capability set; useRepodb_mariadb.detect_ddl_capabilitiesto opt into MariaDB's.- JSON support is backend-specific and should not be treated as portable across backends
See docs/getting-started.md, docs/repo.md, docs/schemas.md,
docs/queries.md, docs/queues.md, and docs/migrations.md for details.
Philosophy #
repodb follows Ecto's philosophy:
- Explicit over implicit - No magic, every operation is visible
- Composable - Small functions that combine well
- Separation of concerns - Domain logic separate from persistence
- Fail fast - Validate early with changesets
License #
ISC License