a database layer insipred by caqti and ecto
1# Transactions
2
3repodb provides two ways to handle transactions: simple transactions via `Repo.transaction` and composable multi-step transactions via the `Multi` module.
4
5## Simple Transactions
6
7For straightforward transactions with a few operations:
8
9```ocaml
10let transfer_funds conn ~from_id ~to_id ~amount =
11 Repo.transaction conn (fun conn ->
12 (* Debit from account *)
13 match Repo.update conn
14 ~table:accounts_table
15 ~columns:["balance"]
16 ~values:[Driver.Value.float (-.amount)]
17 ~where_column:"id"
18 ~where_value:(Driver.Value.int from_id)
19 with
20 | Error e -> Error e
21 | Ok () ->
22 (* Credit to account *)
23 Repo.update conn
24 ~table:accounts_table
25 ~columns:["balance"]
26 ~values:[Driver.Value.float amount]
27 ~where_column:"id"
28 ~where_value:(Driver.Value.int to_id)
29 )
30```
31
32If any operation returns `Error`, the entire transaction is rolled back.
33
34## Multi: Composable Transactions
35
36The `Multi` module enables building complex, multi-step transactions with:
37- Named operations for easy debugging
38- Dependencies between operations
39- Access to previous operation results
40- Automatic rollback on failure
41
42### Setup
43
44```ocaml
45module Multi = Repodb.Multi.Make(Repodb_sqlite)
46(* or *)
47module Multi = Repodb.Multi.Make(Repodb_postgresql)
48```
49
50### Basic Multi
51
52```ocaml
53let create_user_with_profile conn ~name ~email ~bio =
54 let m = Multi.(
55 empty
56 |> insert "user"
57 ~table:users_table
58 ~columns:["name"; "email"]
59 ~values:[Driver.Value.text name; Driver.Value.text email]
60 ) in
61 Multi.execute conn m
62```
63
64### Operations with RETURNING
65
66Get the inserted row back:
67
68```ocaml
69let m = Multi.(
70 empty
71 |> insert_returning "user"
72 ~table:users_table
73 ~columns:["name"; "email"]
74 ~values:[Driver.Value.text "Alice"; Driver.Value.text "alice@example.com"]
75 ~decode:decode_user
76)
77
78match Multi.execute conn m with
79| Ok results ->
80 let user : user = Multi.get_exn results "user" in
81 Printf.printf "Created user with ID: %d\n" user.id
82| Error { failed_operation; error; _ } ->
83 Printf.printf "Failed at %s: %s\n" failed_operation (Error.show_db_error error)
84```
85
86### Dependent Operations
87
88Use `_fn` variants to access results from previous operations:
89
90```ocaml
91let create_user_with_profile conn ~name ~email ~bio =
92 let m = Multi.(
93 empty
94 |> insert_returning "user"
95 ~table:users_table
96 ~columns:["name"; "email"]
97 ~values:[Driver.Value.text name; Driver.Value.text email]
98 ~decode:decode_user
99 |> insert_fn "profile" ~f:(fun results ->
100 let user : user = Multi.get_exn results "user" in
101 (profiles_table,
102 ["user_id"; "bio"],
103 [Driver.Value.int user.id; Driver.Value.text bio])
104 )
105 )
106 in
107 Multi.execute conn m
108```
109
110### All Operation Types
111
112```ocaml
113let m = Multi.(
114 empty
115 (* Insert *)
116 |> insert "op1"
117 ~table:users_table
118 ~columns:["name"]
119 ~values:[Driver.Value.text "Alice"]
120
121 (* Insert with RETURNING *)
122 |> insert_returning "op2"
123 ~table:users_table
124 ~columns:["name"]
125 ~values:[Driver.Value.text "Bob"]
126 ~decode:decode_user
127
128 (* Insert depending on previous result *)
129 |> insert_fn "op3" ~f:(fun results ->
130 let user : user = Multi.get_exn results "op2" in
131 (posts_table, ["author_id"; "title"],
132 [Driver.Value.int user.id; Driver.Value.text "Hello"])
133 )
134
135 (* Insert with RETURNING depending on previous result *)
136 |> insert_returning_fn "op4" ~f:(fun results ->
137 let user : user = Multi.get_exn results "op2" in
138 (posts_table, ["author_id"; "title"],
139 [Driver.Value.int user.id; Driver.Value.text "World"],
140 decode_post)
141 )
142
143 (* Update *)
144 |> update "op5"
145 ~table:users_table
146 ~columns:["name"]
147 ~values:[Driver.Value.text "Alice Updated"]
148 ~where_column:"id"
149 ~where_value:(Driver.Value.int 1)
150
151 (* Update depending on previous result *)
152 |> update_fn "op6" ~f:(fun results ->
153 let user : user = Multi.get_exn results "op2" in
154 (users_table, ["name"], [Driver.Value.text "Updated"],
155 "id", Driver.Value.int user.id)
156 )
157
158 (* Delete *)
159 |> delete "op7"
160 ~table:posts_table
161 ~where_column:"id"
162 ~where_value:(Driver.Value.int 999)
163
164 (* Delete depending on previous result *)
165 |> delete_fn "op8" ~f:(fun results ->
166 let post : post = Multi.get_exn results "op4" in
167 (posts_table, "id", Driver.Value.int post.id)
168 )
169)
170```
171
172### Custom Operations
173
174For complex logic that doesn't fit the predefined operations:
175
176```ocaml
177let m = Multi.(
178 empty
179 |> run "custom_op" ~f:(fun conn results ->
180 (* Full access to connection and all previous results *)
181 let user : user = Multi.get_exn results "user" in
182
183 (* Run any database operation *)
184 match Repo.all_query conn
185 Query.(from posts_table |> where Expr.(raw "author_id" = int user.id))
186 ~decode:decode_post
187 with
188 | Error e -> Error e
189 | Ok posts -> Ok (Multi.Result posts) (* Return packed result *)
190 )
191
192 (* Operation that doesn't return a value *)
193 |> run_no_result "side_effect" ~f:(fun conn results ->
194 (* Do something without returning a value *)
195 match send_welcome_email (Multi.get_exn results "user") with
196 | true -> Ok ()
197 | false -> Error (Error.Query_failed "Email failed")
198 )
199)
200```
201
202### Merging Multis
203
204Combine multiple Multi transactions:
205
206```ocaml
207let create_user = Multi.(
208 empty
209 |> insert_returning "user" ~table:users_table ~columns:["name"]
210 ~values:[Driver.Value.text "Alice"] ~decode:decode_user
211)
212
213let create_profile = Multi.(
214 empty
215 |> insert_fn "profile" ~f:(fun results ->
216 let user : user = Multi.get_exn results "user" in
217 (profiles_table, ["user_id"], [Driver.Value.int user.id])
218 )
219)
220
221let combined = Multi.merge create_user create_profile
222```
223
224### Inspecting Multis
225
226```ocaml
227let names = Multi.names m (* ["user"; "profile"; ...] *)
228let has_user = Multi.has_name "user" m (* true *)
229
230(* Validate before executing *)
231match Multi.validate_multi m with
232| Ok () -> Multi.execute conn m
233| Error msg -> Printf.printf "Invalid multi: %s\n" msg
234```
235
236### Error Handling
237
238```ocaml
239match Multi.execute conn m with
240| Ok results ->
241 (* Access results by name *)
242 let user : user option = Multi.get results "user" in
243 let post : post = Multi.get_exn results "post" in (* raises if missing *)
244 ...
245
246| Error { failed_operation; error; completed } ->
247 Printf.printf "Failed at operation: %s\n" failed_operation;
248 Printf.printf "Error: %s\n" (Error.show_db_error error);
249 (* completed contains results from operations that succeeded before failure *)
250 let partial_user : user option = Multi.get completed "user" in
251 ...
252```
253
254### Working with Results
255
256```ocaml
257(* Safe get - returns option *)
258let user : user option = Multi.get results "user"
259
260(* Unsafe get - raises if not found *)
261let user : user = Multi.get_exn results "user"
262
263(* Add to results manually (in run functions) *)
264let results = Multi.put "key" value results
265```
266
267## Complete Example
268
269```ocaml
270let create_blog_post conn ~author_id ~title ~body ~tags =
271 let m = Multi.(
272 empty
273 (* Create the post *)
274 |> insert_returning "post"
275 ~table:posts_table
276 ~columns:["author_id"; "title"; "body"]
277 ~values:[
278 Driver.Value.int author_id;
279 Driver.Value.text title;
280 Driver.Value.text body;
281 ]
282 ~decode:decode_post
283
284 (* Create tags and associations *)
285 |> run "tags" ~f:(fun conn results ->
286 let post : post = get_exn results "post" in
287
288 (* Insert each tag and create association *)
289 let insert_tag tag_name =
290 match Repo.insert_returning conn
291 ~table:tags_table
292 ~columns:["name"]
293 ~values:[Driver.Value.text tag_name]
294 ~decode:decode_tag
295 with
296 | Error e -> Error e
297 | Ok tag ->
298 Repo.insert conn
299 ~table:post_tags_table
300 ~columns:["post_id"; "tag_id"]
301 ~values:[Driver.Value.int post.id; Driver.Value.int tag.id]
302 in
303
304 let rec process = function
305 | [] -> Ok (Result tags)
306 | tag :: rest ->
307 match insert_tag tag with
308 | Error e -> Error e
309 | Ok () -> process rest
310 in
311 process tags
312 )
313
314 (* Update author's post count *)
315 |> run_no_result "update_count" ~f:(fun conn results ->
316 Repo.exec conn
317 "UPDATE users SET post_count = post_count + 1 WHERE id = $1"
318 ~params:[Driver.Value.int author_id]
319 )
320 )
321 in
322 match Multi.execute conn m with
323 | Ok results -> Ok (Multi.get_exn results "post")
324 | Error { error; _ } -> Error error
325```
326
327## Next Steps
328
329- [Repo](repo.md) - Basic database operations
330- [Migrations](migrations.md) - Database schema versioning
331- [Queries](queries.md) - Building complex queries