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#