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