a database layer insipred by caqti and ecto
1# Getting Started
2
3## Installation
4
5Add repodb to your project:
6
7```bash
8# Core library + SQLite
9opam install repodb repodb-sqlite
10
11# Or with PostgreSQL
12opam install repodb repodb-postgresql
13```
14
15In your `dune` file:
16
17```lisp
18(executable
19 (name myapp)
20 (libraries repodb repodb_sqlite)) ; or repodb_postgresql
21```
22
23## Connecting to a Database
24
25### SQLite
26
27```ocaml
28open Repodb
29
30let () =
31 match Repodb_sqlite.connect "myapp.db" with
32 | Error e -> failwith (Repodb_sqlite.error_message e)
33 | Ok conn ->
34 (* Use connection *)
35 Repodb_sqlite.close conn
36```
37
38### PostgreSQL
39
40```ocaml
41open Repodb
42
43let conninfo = "host=localhost dbname=myapp user=postgres password=secret"
44
45let () =
46 match Repodb_postgresql.connect conninfo with
47 | Error e -> failwith (Repodb_postgresql.error_message e)
48 | Ok conn ->
49 (* Use connection *)
50 Repodb_postgresql.close conn
51```
52
53## Creating the Repo Module
54
55The `Repo` module is parameterized by your driver:
56
57```ocaml
58(* For SQLite *)
59module Repo = Repodb.Repo.Make(Repodb_sqlite)
60
61(* For PostgreSQL *)
62module Repo = Repodb.Repo.Make(Repodb_postgresql)
63```
64
65## Defining Your First Schema
66
67```ocaml
68open Repodb
69
70(* Table definition *)
71let users_table = Schema.table "users"
72
73(* Record type *)
74type user = {
75 id : int;
76 name : string;
77 email : string;
78 age : int;
79}
80
81(* Field definitions with getters/setters *)
82let id_field =
83 Field.make ~table_name:"users" ~name:"id" ~ty:Types.int
84 ~get:(fun u -> u.id)
85 ~set:(fun v u -> { u with id = v })
86 ~primary_key:true ()
87
88let name_field =
89 Field.make ~table_name:"users" ~name:"name" ~ty:Types.string
90 ~get:(fun u -> u.name)
91 ~set:(fun v u -> { u with name = v })
92 ()
93
94let email_field =
95 Field.make ~table_name:"users" ~name:"email" ~ty:Types.string
96 ~get:(fun u -> u.email)
97 ~set:(fun v u -> { u with email = v })
98 ()
99
100let age_field =
101 Field.make ~table_name:"users" ~name:"age" ~ty:Types.int
102 ~get:(fun u -> u.age)
103 ~set:(fun v u -> { u with age = v })
104 ()
105
106(* Decoder function *)
107let decode_user row =
108 {
109 id = Driver.row_int row 0;
110 name = Driver.row_text row 1;
111 email = Driver.row_text row 2;
112 age = Driver.row_int row 3;
113 }
114```
115
116## Basic CRUD Operations
117
118### Insert (Type-Safe)
119
120Use the `Query_values` module for fully type-safe inserts where the compiler verifies column/value types match:
121
122```ocaml
123let insert_user conn ~name ~email ~age =
124 let query =
125 Query.insert_into users_table
126 |> Query_values.values3
127 (name_field, email_field, age_field)
128 (Expr.string name, Expr.string email, Expr.int age)
129 in
130 Repo.exec_query conn query
131```
132
133For bulk inserts:
134
135```ocaml
136let insert_users conn users =
137 let query =
138 Query.insert_into users_table
139 |> Query_values.values3_multi
140 (name_field, email_field, age_field)
141 (List.map (fun (name, email, age) ->
142 (Expr.string name, Expr.string email, Expr.int age)) users)
143 in
144 Repo.exec_query conn query
145```
146
147See [Queries - Type-Safe INSERT](queries.md#insert-type-safe) for more details.
148
149### Query All
150
151```ocaml
152let all_users conn =
153 Repo.all conn ~table:users_table ~decode:decode_user
154```
155
156### Query One by ID
157
158```ocaml
159let get_user conn id =
160 Repo.get conn ~table:users_table ~id ~decode:decode_user
161```
162
163### Update
164
165```ocaml
166let update_user_age conn ~id ~new_age =
167 Repo.update conn
168 ~table:users_table
169 ~columns:["age"]
170 ~values:[Driver.Value.int new_age]
171 ~where_column:"id"
172 ~where_value:(Driver.Value.int id)
173```
174
175### Delete
176
177```ocaml
178let delete_user conn id =
179 Repo.delete conn
180 ~table:users_table
181 ~where_column:"id"
182 ~where_value:(Driver.Value.int id)
183```
184
185## Using the Query DSL (Type-Safe)
186
187For more complex queries, use the Query module with your field definitions for full type safety:
188
189```ocaml
190let adults conn =
191 let query = Query.(
192 from users_table
193 |> where Expr.(column age_field >= int 18)
194 |> order_by ~direction:Asc (Expr.column name_field)
195 |> limit 100
196 ) in
197 Repo.all_query conn query ~decode:decode_user
198```
199
200Using `Expr.column field` instead of `Expr.raw "column_name"` gives you:
201- **Compile-time type checking** - The compiler ensures you use the right types
202- **Refactoring safety** - Rename a field and all usages update automatically
203- **IDE support** - Jump to definition, find usages, etc.
204
205See [Queries](queries.md) for the full Query DSL reference.
206
207## Using Changesets
208
209Validate data before inserting:
210
211```ocaml
212let create_user_changeset params =
213 let empty_user = { id = 0; name = ""; email = ""; age = 0 } in
214 Changeset.create empty_user
215 |> Changeset.cast params ~fields:[name_field; email_field; age_field]
216 |> Changeset.validate_required [name_field; email_field]
217 |> Changeset.validate_format email_field ~pattern:"^[^@]+@[^@]+$"
218 |> Changeset.validate_number age_field ~greater_than_or_equal:0
219
220let insert_with_validation conn params =
221 let cs = create_user_changeset params in
222 if Changeset.is_valid cs then
223 let user = Changeset.data cs in
224 insert_user conn ~name:user.name ~email:user.email ~age:user.age
225 else
226 Error (Error.Validation_failed (Changeset.error_messages cs))
227```
228
229## Next Steps
230
231- [Schemas](schemas.md) - Learn more about schema definitions
232- [Changesets](changesets.md) - Deep dive into validation
233- [Queries](queries.md) - Master the query DSL
234- [Associations](associations.md) - Define relationships