a database layer insipred by caqti and ecto
Getting Started#
Installation#
Add repodb to your project:
# Core library + SQLite
opam install repodb repodb-sqlite
# Or with PostgreSQL
opam install repodb repodb-postgresql
In your dune file:
(executable
(name myapp)
(libraries repodb repodb_sqlite)) ; or repodb_postgresql
Connecting to a Database#
SQLite#
open Repodb
let () =
match Repodb_sqlite.connect "myapp.db" with
| Error e -> failwith (Repodb_sqlite.error_message e)
| Ok conn ->
(* Use connection *)
Repodb_sqlite.close conn
PostgreSQL#
open Repodb
let conninfo = "host=localhost dbname=myapp user=postgres password=secret"
let () =
match Repodb_postgresql.connect conninfo with
| Error e -> failwith (Repodb_postgresql.error_message e)
| Ok conn ->
(* Use connection *)
Repodb_postgresql.close conn
Creating the Repo Module#
The Repo module is parameterized by your driver:
(* For SQLite *)
module Repo = Repodb.Repo.Make(Repodb_sqlite)
(* For PostgreSQL *)
module Repo = Repodb.Repo.Make(Repodb_postgresql)
Defining Your First Schema#
open Repodb
(* Table definition *)
let users_table = Schema.table "users"
(* Record type *)
type user = {
id : int;
name : string;
email : string;
age : int;
}
(* Field definitions with getters/setters *)
let id_field =
Field.make ~table_name:"users" ~name:"id" ~ty:Types.int
~get:(fun u -> u.id)
~set:(fun v u -> { u with id = v })
~primary_key:true ()
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 email_field =
Field.make ~table_name:"users" ~name:"email" ~ty:Types.string
~get:(fun u -> u.email)
~set:(fun v u -> { u with email = 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 })
()
(* Decoder function *)
let decode_user row =
{
id = Driver.row_int row 0;
name = Driver.row_text row 1;
email = Driver.row_text row 2;
age = Driver.row_int row 3;
}
Basic CRUD Operations#
Insert (Type-Safe)#
Use the Query_values module for fully type-safe inserts where the compiler verifies column/value types match:
let insert_user conn ~name ~email ~age =
let query =
Query.insert_into users_table
|> Query_values.values3
(name_field, email_field, age_field)
(Expr.string name, Expr.string email, Expr.int age)
in
Repo.exec_query conn query
For bulk inserts:
let insert_users conn users =
let query =
Query.insert_into users_table
|> Query_values.values3_multi
(name_field, email_field, age_field)
(List.map (fun (name, email, age) ->
(Expr.string name, Expr.string email, Expr.int age)) users)
in
Repo.exec_query conn query
See Queries - Type-Safe INSERT for more details.
Query All#
let all_users conn =
Repo.all conn ~table:users_table ~decode:decode_user
Query One by ID#
let get_user conn id =
Repo.get conn ~table:users_table ~id ~decode:decode_user
Update#
let update_user_age conn ~id ~new_age =
Repo.update conn
~table:users_table
~columns:["age"]
~values:[Driver.Value.int new_age]
~where_column:"id"
~where_value:(Driver.Value.int id)
Delete#
let delete_user conn id =
Repo.delete conn
~table:users_table
~where_column:"id"
~where_value:(Driver.Value.int id)
Using the Query DSL (Type-Safe)#
For more complex queries, use the Query module with your field definitions for full type safety:
let adults conn =
let query = Query.(
from users_table
|> where Expr.(column age_field >= int 18)
|> order_by ~direction:Asc (Expr.column name_field)
|> limit 100
) in
Repo.all_query conn query ~decode:decode_user
Using Expr.column field instead of Expr.raw "column_name" gives you:
- Compile-time type checking - The compiler ensures you use the right types
- Refactoring safety - Rename a field and all usages update automatically
- IDE support - Jump to definition, find usages, etc.
See Queries for the full Query DSL reference.
Using Changesets#
Validate data before inserting:
let create_user_changeset params =
let empty_user = { id = 0; name = ""; email = ""; age = 0 } in
Changeset.create empty_user
|> Changeset.cast params ~fields:[name_field; email_field; age_field]
|> Changeset.validate_required [name_field; email_field]
|> Changeset.validate_format email_field ~pattern:"^[^@]+@[^@]+$"
|> Changeset.validate_number age_field ~greater_than_or_equal:0
let insert_with_validation conn params =
let cs = create_user_changeset params in
if Changeset.is_valid cs then
let user = Changeset.data cs in
insert_user conn ~name:user.name ~email:user.email ~age:user.age
else
Error (Error.Validation_failed (Changeset.error_messages cs))
Next Steps#
- Schemas - Learn more about schema definitions
- Changesets - Deep dive into validation
- Queries - Master the query DSL
- Associations - Define relationships