a database layer insipred by caqti and ecto
1# Queries
2
3The Query DSL lets you build type-safe, composable SQL queries using your schema definitions.
4
5## Schema Integration
6
7repodb queries leverage your field definitions for type safety. Instead of using raw column names, use `Expr.column` with your field definitions:
8
9```ocaml
10open Repodb
11
12(* Define your schema *)
13type user = { id : int; name : string; email : string; age : int }
14
15let users_table = Schema.table "users"
16
17let id_field =
18 Field.make ~table_name:"users" ~name:"id" ~ty:Types.int
19 ~get:(fun u -> u.id) ~set:(fun v u -> { u with id = v })
20 ~primary_key:true ()
21
22let name_field =
23 Field.make ~table_name:"users" ~name:"name" ~ty:Types.string
24 ~get:(fun u -> u.name) ~set:(fun v u -> { u with name = v }) ()
25
26let email_field =
27 Field.make ~table_name:"users" ~name:"email" ~ty:Types.string
28 ~get:(fun u -> u.email) ~set:(fun v u -> { u with email = v }) ()
29
30let age_field =
31 Field.make ~table_name:"users" ~name:"age" ~ty:Types.int
32 ~get:(fun u -> u.age) ~set:(fun v u -> { u with age = v }) ()
33```
34
35Now use these fields in your queries:
36
37```ocaml
38(* Type-safe query using field definitions *)
39let adults = Query.(
40 from users_table
41 |> where Expr.(column age_field >= int 18)
42 |> order_by (Expr.column name_field)
43 |> limit 10
44)
45```
46
47### Why Use `Expr.column` Instead of `Expr.raw`?
48
49| Approach | Type Safety | Refactoring | Example |
50|----------|-------------|-------------|---------|
51| `Expr.column field` | ✅ Compile-time checked | ✅ Rename field, all usages update | `Expr.column age_field` |
52| `Expr.raw "name"` | ❌ No checking | ❌ Manual find/replace | `Expr.raw "age"` |
53
54```ocaml
55(* RECOMMENDED: Type-safe, refactor-friendly *)
56Query.where Expr.(column age_field >= int 18)
57
58(* NOT RECOMMENDED: Stringly-typed, error-prone *)
59Query.where Expr.(raw "age" >= int 18)
60```
61
62## Basic Queries
63
64### SELECT
65
66```ocaml
67(* Select all columns *)
68let query = Query.from users_table
69
70(* Select specific columns using fields *)
71let query = Query.(
72 from users_table
73 |> select Expr.[column name_field; column email_field]
74)
75```
76
77### INSERT (Type-Safe)
78
79The `Query_values` module provides **fully type-safe** INSERT statements. The compiler verifies:
80- Column count matches value count
81- Each value type matches its field type
82- All fields belong to the same table
83
84```ocaml
85(* Type-safe insert with heterogeneous column types *)
86let query =
87 Query.insert_into users_table
88 |> Query_values.values4
89 (id_field, name_field, email_field, age_field)
90 (Expr.int 1, Expr.string "Alice", Expr.string "alice@example.com", Expr.int 25)
91
92(* Multi-row insert - also fully type-safe *)
93let query =
94 Query.insert_into users_table
95 |> Query_values.values3_multi
96 (name_field, email_field, age_field)
97 [
98 (Expr.string "Alice", Expr.string "alice@example.com", Expr.int 25);
99 (Expr.string "Bob", Expr.string "bob@example.com", Expr.int 30);
100 (Expr.string "Carol", Expr.string "carol@example.com", Expr.int 28);
101 ]
102```
103
104Functions are available from `values1` to `values30` for single rows, and `values1_multi` to `values30_multi` for bulk inserts.
105
106#### Why Type-Safe INSERTs Matter
107
108With the legacy `Query.values` approach, the compiler cannot catch these bugs:
109
110```ocaml
111(* DANGEROUS: These compile but are wrong! *)
112
113(* Bug 1: Column/value count mismatch *)
114Query.values [name_field; email_field] [[Expr.string "Alice"]] (* Missing value! *)
115
116(* Bug 2: Type mismatch - swapped string and int *)
117Query.values [name_field; age_field] [[Expr.int 25; Expr.string "Alice"]]
118
119(* Bug 3: Swapped columns of same type *)
120Query.values [email_field; name_field] [[Expr.string "Alice"; Expr.string "alice@example.com"]]
121```
122
123With `Query_values`, all these are **compile-time errors**:
124
125```ocaml
126(* All of these fail to compile *)
127
128(* Error: expects 2-tuple, got 1 value *)
129Query_values.values2 (name_field, email_field) (Expr.string "Alice")
130
131(* Error: int Expr.t is not string Expr.t *)
132Query_values.values2 (name_field, age_field) (Expr.int 25, Expr.string "Alice")
133```
134
135### INSERT (Legacy)
136
137The original `Query.values` function is still available but provides weaker type safety:
138
139```ocaml
140(* Works but no compile-time type checking between columns and values *)
141let query = Query.(
142 insert_into users_table
143 |> values [name_field; email_field; age_field] [
144 [Expr.string "Alice"; Expr.string "alice@example.com"; Expr.int 25]
145 ]
146)
147```
148
149### UPDATE
150
151```ocaml
152let query = Query.(
153 update users_table
154 |> set name_field (Expr.string "Bob")
155 |> set age_field (Expr.int 30)
156 |> where Expr.(column id_field = int 1)
157)
158```
159
160### DELETE
161
162```ocaml
163let query = Query.(
164 delete_from users_table
165 |> where Expr.(column id_field = int 1)
166)
167```
168
169## WHERE Clauses
170
171### Basic Conditions
172
173```ocaml
174let query = Query.(
175 from users_table
176 |> where Expr.(column age_field >= int 18)
177)
178```
179
180### Multiple Conditions (AND)
181
182```ocaml
183(* Assuming you have a status_field defined *)
184let query = Query.(
185 from users_table
186 |> where Expr.(column age_field >= int 18)
187 |> and_where Expr.(column status_field = string "active")
188)
189```
190
191### OR Conditions
192
193```ocaml
194(* Assuming you have a role_field defined *)
195let query = Query.(
196 from users_table
197 |> where Expr.(column role_field = string "admin")
198 |> or_where Expr.(column role_field = string "moderator")
199)
200```
201
202## Expressions
203
204The `Expr` module provides type-safe expression building.
205
206### Literals
207
208```ocaml
209Expr.int 42
210Expr.int64 42L
211Expr.float 3.14
212Expr.string "hello"
213Expr.bool true
214Expr.null Types.string (* NULL of type string *)
215```
216
217### Column References
218
219```ocaml
220(* RECOMMENDED: Using field definition - type-safe *)
221Expr.column name_field
222Expr.column age_field
223
224(* NOT RECOMMENDED: Using raw column name - no type checking *)
225Expr.raw "name"
226Expr.raw "users.name" (* qualified name for joins *)
227```
228
229### Comparison Operators
230
231```ocaml
232open Expr
233
234(* Using fields (recommended) *)
235column age_field = int 25
236column name_field <> string "admin"
237column age_field > int 18
238column age_field >= int 18
239column age_field < int 65
240column age_field <= int 65
241
242(* Function syntax *)
243eq (column age_field) (int 25)
244neq (column name_field) (string "admin")
245gt (column age_field) (int 18)
246gte (column age_field) (int 18)
247lt (column age_field) (int 65)
248lte (column age_field) (int 65)
249```
250
251### Logical Operators
252
253```ocaml
254open Expr
255
256(* AND *)
257(column age_field >= int 18) && (column status_field = string "active")
258and_ (column age_field >= int 18) (column status_field = string "active")
259
260(* OR *)
261(column role_field = string "admin") || (column role_field = string "mod")
262or_ (column role_field = string "admin") (column role_field = string "mod")
263
264(* NOT *)
265not_ (column deleted_field = bool true)
266```
267
268### NULL Checks
269
270```ocaml
271Expr.is_null (Expr.column deleted_at_field)
272Expr.is_not_null (Expr.column email_field)
273```
274
275### BETWEEN
276
277```ocaml
278Expr.between (Expr.column age_field) (Expr.int 18) (Expr.int 65)
279```
280
281### IN Lists
282
283```ocaml
284Expr.in_list (Expr.column status_field) [
285 Expr.string "active";
286 Expr.string "pending"
287]
288
289Expr.not_in_list (Expr.column role_field) [
290 Expr.string "banned";
291 Expr.string "suspended"
292]
293```
294
295### LIKE / Pattern Matching
296
297```ocaml
298Expr.like (Expr.column name_field) "%alice%"
299Expr.ilike (Expr.column name_field) "%alice%" (* case-insensitive, PostgreSQL *)
300Expr.similar_to (Expr.column email_field) "%@example.com"
301Expr.regexp (Expr.column name_field) "^[A-Z]" (* PostgreSQL regex *)
302```
303
304### Arithmetic
305
306```ocaml
307open Expr
308
309(* Integer arithmetic *)
310column price_field + int 10
311column price_field - int 5
312column quantity_field * int 2
313column total_field / int 4
314column count_field mod int 3
315
316(* Float arithmetic *)
317column price_field +. float 10.5
318column discount_field *. float 0.9
319```
320
321### String Functions
322
323```ocaml
324Expr.lower (Expr.column name_field)
325Expr.upper (Expr.column name_field)
326Expr.length (Expr.column description_field)
327Expr.trim (Expr.column input_field)
328Expr.ltrim (Expr.column input_field)
329Expr.rtrim (Expr.column input_field)
330Expr.concat [Expr.column first_name_field; Expr.string " "; Expr.column last_name_field]
331Expr.concat_ws ", " [Expr.column city_field; Expr.column state_field; Expr.column country_field]
332Expr.substring (Expr.column name_field) ~from:1 ~for_:3
333Expr.replace (Expr.column text_field) ~from:"old" ~to_:"new"
334Expr.left (Expr.column name_field) 5
335Expr.right (Expr.column name_field) 3
336```
337
338### Aggregate Functions
339
340```ocaml
341Expr.count (Expr.column id_field)
342Expr.count_all
343Expr.count_distinct (Expr.column category_field)
344Expr.sum (Expr.column amount_field)
345Expr.avg (Expr.column price_field)
346Expr.max_ (Expr.column created_at_field) Types.ptime
347Expr.min_ (Expr.column price_field) Types.float
348```
349
350### Date/Time Functions
351
352```ocaml
353Expr.now ()
354Expr.current_date
355Expr.current_timestamp
356Expr.date_part "year" (Expr.column created_at_field)
357Expr.extract "month" (Expr.column created_at_field)
358Expr.age (Expr.column created_at_field) (Expr.now ())
359```
360
361### Math Functions
362
363```ocaml
364Expr.abs_ (Expr.column balance_field)
365Expr.ceil (Expr.column price_field)
366Expr.floor (Expr.column price_field)
367Expr.round (Expr.column average_field)
368Expr.sqrt (Expr.column variance_field)
369Expr.power (Expr.column base_field) (Expr.int 2)
370Expr.random
371```
372
373### Conditional Expressions
374
375```ocaml
376(* COALESCE - return first non-null *)
377Expr.coalesce [Expr.column nickname_field; Expr.column name_field] Types.string
378
379(* NULLIF - return null if equal *)
380Expr.nullif (Expr.column value_field) (Expr.int 0) Types.int
381
382(* GREATEST / LEAST *)
383Expr.greatest [Expr.column a_field; Expr.column b_field; Expr.column c_field] Types.int
384Expr.least [Expr.column x_field; Expr.column y_field] Types.float
385
386(* CASE WHEN *)
387Expr.case [
388 (Expr.(column status_field = string "active"), Expr.string "Active");
389 (Expr.(column status_field = string "pending"), Expr.string "Pending");
390] ~else_:(Expr.string "Unknown") Types.string
391```
392
393### Type Casting
394
395```ocaml
396Expr.cast (Expr.column price_field) Types.int
397```
398
399### Subqueries
400
401```ocaml
402Expr.subquery "SELECT MAX(price) FROM products"
403```
404
405## ORDER BY
406
407```ocaml
408(* Default ascending *)
409let query = Query.(
410 from users_table
411 |> order_by (Expr.column name_field)
412)
413
414(* Explicit direction *)
415let query = Query.(
416 from users_table
417 |> order_by ~direction:Desc (Expr.column created_at_field)
418)
419
420(* Helper functions *)
421let query = Query.(
422 from users_table
423 |> asc (Expr.column name_field)
424 |> desc (Expr.column created_at_field)
425)
426
427(* Multiple columns *)
428let query = Query.(
429 from users_table
430 |> order_by ~direction:Asc (Expr.column last_name_field)
431 |> order_by ~direction:Asc (Expr.column first_name_field)
432)
433```
434
435## LIMIT and OFFSET
436
437```ocaml
438let query = Query.(
439 from users_table
440 |> order_by (Expr.column id_field)
441 |> limit 10
442 |> offset 20
443)
444```
445
446## DISTINCT
447
448```ocaml
449let query = Query.(
450 from users_table
451 |> distinct
452 |> select Expr.[column category_field]
453)
454```
455
456## JOINs
457
458For joins, you'll typically have fields from multiple tables:
459
460```ocaml
461(* Posts schema *)
462let posts_table = Schema.table "posts"
463
464let post_id_field =
465 Field.make ~table_name:"posts" ~name:"id" ~ty:Types.int
466 ~get:(fun p -> p.id) ~set:(fun v p -> { p with id = v })
467 ~primary_key:true ()
468
469let author_id_field =
470 Field.make ~table_name:"posts" ~name:"author_id" ~ty:Types.int
471 ~get:(fun p -> p.author_id) ~set:(fun v p -> { p with author_id = v }) ()
472```
473
474### INNER JOIN
475
476```ocaml
477let query = Query.(
478 from users_table
479 |> inner_join posts_table
480 ~on:Expr.(column id_field = column author_id_field)
481)
482```
483
484### LEFT JOIN
485
486```ocaml
487let query = Query.(
488 from users_table
489 |> left_join posts_table
490 ~on:Expr.(column id_field = column author_id_field)
491)
492```
493
494### Other Joins
495
496```ocaml
497Query.right_join table ~on:condition query
498Query.full_join table ~on:condition query
499Query.join ~kind:Inner table ~on:condition query
500```
501
502## GROUP BY and HAVING
503
504```ocaml
505let query = Query.(
506 from posts_table
507 |> select Expr.[column author_id_field; count (column post_id_field)]
508 |> group_by Expr.[column author_id_field]
509 |> having Expr.(count (column post_id_field) > int 5)
510)
511```
512
513## RETURNING (PostgreSQL)
514
515```ocaml
516let query = Query.(
517 insert_into users_table
518 |> Query_values.values2
519 (name_field, email_field)
520 (Expr.string "Alice", Expr.string "a@b.com")
521 |> returning Expr.[column id_field; column created_at_field]
522)
523```
524
525## ON CONFLICT (Upsert)
526
527### Do Nothing
528
529```ocaml
530let query = Query.(
531 insert_into users_table
532 |> Query_values.values2
533 (email_field, name_field)
534 (Expr.string "a@b.com", Expr.string "Alice")
535 |> on_conflict_do_nothing ~target:[email_field]
536)
537```
538
539### Do Update
540
541```ocaml
542let query = Query.(
543 insert_into users_table
544 |> Query_values.values2
545 (email_field, name_field)
546 (Expr.string "a@b.com", Expr.string "Alice")
547 |> on_conflict_do_update
548 ~target:[email_field]
549 ~set:[(name_field, Expr.string "Alice Updated")]
550)
551```
552
553## Generating SQL
554
555```ocaml
556let sql = Query.to_sql query
557(* "SELECT * FROM users WHERE users.age >= 18 ORDER BY users.name ASC LIMIT 10" *)
558```
559
560## Executing Queries
561
562See [Repo](repo.md) for executing queries:
563
564```ocaml
565let users = Repo.all_query conn query ~decode:decode_user
566```
567
568## Summary: Type-Safe Query Patterns
569
570| Operation | Type-Safe Approach |
571|-----------|-------------------|
572| Column reference | `Expr.column field` |
573| WHERE condition | `Query.where Expr.(column field = value)` |
574| INSERT | `Query_values.valuesN (fields) (values)` |
575| UPDATE SET | `Query.set field value` |
576| ORDER BY | `Query.order_by (Expr.column field)` |
577| SELECT | `Query.select Expr.[column f1; column f2]` |
578| JOIN ON | `Query.inner_join t ~on:Expr.(column f1 = column f2)` |
579| ON CONFLICT | `Query.on_conflict_do_nothing ~target:[field]` |
580
581## Next Steps
582
583- [Repo](repo.md) - Execute queries and perform CRUD operations
584- [Associations](associations.md) - Work with relationships
585- [Transactions](transactions.md) - Transaction handling