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