a database layer insipred by caqti and ecto
1let conninfo =
2 match Sys.getenv_opt "REPODB_PG_CONNINFO" with
3 | Some s -> s
4 | None ->
5 "host=localhost port=5432 dbname=repodb_test user=repodb password=repodb"
6
7let setup () =
8 match Repodb_postgresql.connect conninfo with
9 | Error e -> failwith (Repodb_postgresql.error_message e)
10 | Ok conn ->
11 let _ =
12 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS users" ~params:[||]
13 in
14 let _ =
15 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS nums" ~params:[||]
16 in
17 let _ =
18 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS flags" ~params:[||]
19 in
20 let _ =
21 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS measurements"
22 ~params:[||]
23 in
24 conn
25
26let teardown conn =
27 let _ =
28 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS users" ~params:[||]
29 in
30 let _ =
31 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS nums" ~params:[||]
32 in
33 let _ =
34 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS flags" ~params:[||]
35 in
36 let _ =
37 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS measurements" ~params:[||]
38 in
39 Repodb_postgresql.close conn
40
41let with_db f () =
42 let conn = setup () in
43 Fun.protect ~finally:(fun () -> teardown conn) (fun () -> f conn)
44
45let test_connect =
46 with_db (fun conn ->
47 let result = Repodb_postgresql.exec conn "SELECT 1" ~params:[||] in
48 match result with
49 | Ok () -> ()
50 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e))
51
52let test_create_table =
53 with_db (fun conn ->
54 let sql =
55 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email \
56 TEXT, age INTEGER)"
57 in
58 match Repodb_postgresql.exec conn sql ~params:[||] with
59 | Ok () -> ()
60 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e))
61
62let test_insert_and_query =
63 with_db (fun conn ->
64 let _ =
65 Repodb_postgresql.exec conn
66 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, \
67 email TEXT, age INTEGER)"
68 ~params:[||]
69 in
70 let insert =
71 Repodb_postgresql.exec conn
72 "INSERT INTO users (name, email, age) VALUES ($1, $2, $3)"
73 ~params:
74 [|
75 Repodb.Driver.Value.text "Alice";
76 Repodb.Driver.Value.text "alice@example.com";
77 Repodb.Driver.Value.int 30;
78 |]
79 in
80 (match insert with
81 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
82 | Ok () -> ());
83 match Repodb_postgresql.query conn "SELECT * FROM users" ~params:[||] with
84 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
85 | Ok rows ->
86 Alcotest.(check int) "one row" 1 (List.length rows);
87 let row = List.hd rows in
88 Alcotest.(check string) "name" "Alice" (Repodb.Driver.row_text row 1))
89
90let test_query_one =
91 with_db (fun conn ->
92 let _ =
93 Repodb_postgresql.exec conn
94 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
95 in
96 let _ =
97 Repodb_postgresql.exec conn "INSERT INTO users (name) VALUES ($1)"
98 ~params:[| Repodb.Driver.Value.text "Bob" |]
99 in
100 match
101 Repodb_postgresql.query_one conn "SELECT * FROM users WHERE name = $1"
102 ~params:[| Repodb.Driver.Value.text "Bob" |]
103 with
104 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
105 | Ok None -> Alcotest.fail "expected row"
106 | Ok (Some row) ->
107 Alcotest.(check string) "name" "Bob" (Repodb.Driver.row_text row 1))
108
109let test_query_one_not_found =
110 with_db (fun conn ->
111 let _ =
112 Repodb_postgresql.exec conn
113 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
114 in
115 match
116 Repodb_postgresql.query_one conn "SELECT * FROM users WHERE name = $1"
117 ~params:[| Repodb.Driver.Value.text "Nobody" |]
118 with
119 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
120 | Ok None -> ()
121 | Ok (Some _) -> Alcotest.fail "expected no row")
122
123let test_update =
124 with_db (fun conn ->
125 let _ =
126 Repodb_postgresql.exec conn
127 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, age INTEGER)"
128 ~params:[||]
129 in
130 let _ =
131 Repodb_postgresql.exec conn
132 "INSERT INTO users (name, age) VALUES ($1, $2)"
133 ~params:
134 [| Repodb.Driver.Value.text "Carol"; Repodb.Driver.Value.int 25 |]
135 in
136 let _ =
137 Repodb_postgresql.exec conn "UPDATE users SET age = $1 WHERE name = $2"
138 ~params:
139 [| Repodb.Driver.Value.int 26; Repodb.Driver.Value.text "Carol" |]
140 in
141 match
142 Repodb_postgresql.query_one conn "SELECT age FROM users WHERE name = $1"
143 ~params:[| Repodb.Driver.Value.text "Carol" |]
144 with
145 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
146 | Ok None -> Alcotest.fail "expected row"
147 | Ok (Some row) ->
148 Alcotest.(check int) "age" 26 (Repodb.Driver.row_int row 0))
149
150let test_delete =
151 with_db (fun conn ->
152 let _ =
153 Repodb_postgresql.exec conn
154 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
155 in
156 let _ =
157 Repodb_postgresql.exec conn "INSERT INTO users (name) VALUES ($1)"
158 ~params:[| Repodb.Driver.Value.text "Dave" |]
159 in
160 let _ =
161 Repodb_postgresql.exec conn "DELETE FROM users WHERE name = $1"
162 ~params:[| Repodb.Driver.Value.text "Dave" |]
163 in
164 match Repodb_postgresql.query conn "SELECT * FROM users" ~params:[||] with
165 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
166 | Ok rows -> Alcotest.(check int) "no rows" 0 (List.length rows))
167
168let test_transaction_commit =
169 with_db (fun conn ->
170 let _ =
171 Repodb_postgresql.exec conn
172 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
173 in
174 let result =
175 Repodb_postgresql.transaction conn (fun conn ->
176 let _ =
177 Repodb_postgresql.exec conn "INSERT INTO users (name) VALUES ($1)"
178 ~params:[| Repodb.Driver.Value.text "Eve" |]
179 in
180 Ok "done")
181 in
182 (match result with
183 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
184 | Ok _ -> ());
185 match Repodb_postgresql.query conn "SELECT * FROM users" ~params:[||] with
186 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
187 | Ok rows -> Alcotest.(check int) "one row" 1 (List.length rows))
188
189let test_transaction_rollback =
190 with_db (fun conn ->
191 let _ =
192 Repodb_postgresql.exec conn
193 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)" ~params:[||]
194 in
195 let result =
196 Repodb_postgresql.transaction conn (fun conn ->
197 let _ =
198 Repodb_postgresql.exec conn
199 "INSERT INTO users (id, name) VALUES ($1, $2)"
200 ~params:
201 [|
202 Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Frank";
203 |]
204 in
205 Repodb_postgresql.exec conn
206 "INSERT INTO users (id, name) VALUES ($1, $2)"
207 ~params:
208 [|
209 Repodb.Driver.Value.int 1;
210 Repodb.Driver.Value.text "Duplicate";
211 |])
212 in
213 (match result with
214 | Ok _ -> Alcotest.fail "expected error from duplicate key"
215 | Error _ -> ());
216 match Repodb_postgresql.query conn "SELECT * FROM users" ~params:[||] with
217 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
218 | Ok rows ->
219 Alcotest.(check int) "no rows (rolled back)" 0 (List.length rows))
220
221let test_query_fold =
222 with_db (fun conn ->
223 let _ =
224 Repodb_postgresql.exec conn
225 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
226 in
227 for i = 1 to 5 do
228 let _ =
229 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
230 ~params:[| Repodb.Driver.Value.int i |]
231 in
232 ()
233 done;
234 match
235 Repodb_postgresql.query_fold conn "SELECT val FROM nums" ~params:[||]
236 ~init:0 ~f:(fun acc row -> acc + Repodb.Driver.row_int row 0)
237 with
238 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
239 | Ok sum -> Alcotest.(check int) "sum 1..5" 15 sum)
240
241let test_query_iter =
242 with_db (fun conn ->
243 let _ =
244 Repodb_postgresql.exec conn
245 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
246 in
247 for i = 1 to 3 do
248 let _ =
249 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
250 ~params:[| Repodb.Driver.Value.int i |]
251 in
252 ()
253 done;
254 let count = ref 0 in
255 match
256 Repodb_postgresql.query_iter conn "SELECT val FROM nums" ~params:[||]
257 ~f:(fun _ -> incr count)
258 with
259 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
260 | Ok () -> Alcotest.(check int) "iterated 3" 3 !count)
261
262let test_returning =
263 with_db (fun conn ->
264 let _ =
265 Repodb_postgresql.exec conn
266 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
267 in
268 match
269 Repodb_postgresql.query_one conn
270 "INSERT INTO users (name) VALUES ($1) RETURNING id"
271 ~params:[| Repodb.Driver.Value.text "Grace" |]
272 with
273 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
274 | Ok None -> Alcotest.fail "expected row"
275 | Ok (Some row) ->
276 let id = Repodb.Driver.row_int row 0 in
277 Alcotest.(check bool) "id > 0" true (id > 0))
278
279let test_null_handling =
280 with_db (fun conn ->
281 let _ =
282 Repodb_postgresql.exec conn
283 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT)"
284 ~params:[||]
285 in
286 let _ =
287 Repodb_postgresql.exec conn
288 "INSERT INTO users (name, email) VALUES ($1, NULL)"
289 ~params:[| Repodb.Driver.Value.text "Henry" |]
290 in
291 match
292 Repodb_postgresql.query_one conn
293 "SELECT email FROM users WHERE name = $1"
294 ~params:[| Repodb.Driver.Value.text "Henry" |]
295 with
296 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
297 | Ok None -> Alcotest.fail "expected row"
298 | Ok (Some row) ->
299 Alcotest.(check bool)
300 "email is null" true
301 (Repodb.Driver.row_is_null row 0))
302
303let test_bool_values =
304 with_db (fun conn ->
305 let _ =
306 Repodb_postgresql.exec conn
307 "CREATE TABLE flags (id SERIAL PRIMARY KEY, active BOOLEAN)"
308 ~params:[||]
309 in
310 let _ =
311 Repodb_postgresql.exec conn "INSERT INTO flags (active) VALUES ($1)"
312 ~params:[| Repodb.Driver.Value.bool true |]
313 in
314 let _ =
315 Repodb_postgresql.exec conn "INSERT INTO flags (active) VALUES ($1)"
316 ~params:[| Repodb.Driver.Value.bool false |]
317 in
318 match
319 Repodb_postgresql.query conn "SELECT active FROM flags ORDER BY id"
320 ~params:[||]
321 with
322 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
323 | Ok rows ->
324 Alcotest.(check int) "two rows" 2 (List.length rows);
325 Alcotest.(check bool)
326 "first is true" true
327 (Repodb.Driver.row_bool (List.nth rows 0) 0);
328 Alcotest.(check bool)
329 "second is false" false
330 (Repodb.Driver.row_bool (List.nth rows 1) 0))
331
332let test_float_values =
333 with_db (fun conn ->
334 let _ =
335 Repodb_postgresql.exec conn
336 "CREATE TABLE measurements (id SERIAL PRIMARY KEY, value DOUBLE \
337 PRECISION)"
338 ~params:[||]
339 in
340 let _ =
341 Repodb_postgresql.exec conn
342 "INSERT INTO measurements (value) VALUES ($1)"
343 ~params:[| Repodb.Driver.Value.float 3.14159 |]
344 in
345 match
346 Repodb_postgresql.query_one conn "SELECT value FROM measurements"
347 ~params:[||]
348 with
349 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
350 | Ok None -> Alcotest.fail "expected row"
351 | Ok (Some row) ->
352 let v = Repodb.Driver.row_float row 0 in
353 Alcotest.(check bool)
354 "close to pi" true
355 (abs_float (v -. 3.14159) < 0.0001))
356
357let test_with_connection () =
358 Repodb_postgresql.with_connection conninfo (fun conn ->
359 match Repodb_postgresql.exec conn "SELECT 1" ~params:[||] with
360 | Ok () -> Ok ()
361 | Error e -> Error e)
362 |> function
363 | Ok () -> ()
364 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
365
366let test_placeholder () =
367 Alcotest.(check string) "$1" "$1" (Repodb_postgresql.placeholder 1);
368 Alcotest.(check string) "$2" "$2" (Repodb_postgresql.placeholder 2);
369 Alcotest.(check string) "$10" "$10" (Repodb_postgresql.placeholder 10)
370
371let test_create_index =
372 with_db (fun conn ->
373 let _ =
374 Repodb_postgresql.exec conn
375 "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL)"
376 ~params:[||]
377 in
378 match
379 Repodb_postgresql.exec conn
380 "CREATE INDEX idx_users_email ON users (email)" ~params:[||]
381 with
382 | Ok () -> ()
383 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e))
384
385let test_create_unique_index =
386 with_db (fun conn ->
387 let _ =
388 Repodb_postgresql.exec conn
389 "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL)"
390 ~params:[||]
391 in
392 let _ =
393 Repodb_postgresql.exec conn
394 "CREATE UNIQUE INDEX idx_users_email_unique ON users (email)"
395 ~params:[||]
396 in
397 let _ =
398 Repodb_postgresql.exec conn "INSERT INTO users (email) VALUES ($1)"
399 ~params:[| Repodb.Driver.Value.text "test@example.com" |]
400 in
401 match
402 Repodb_postgresql.exec conn "INSERT INTO users (email) VALUES ($1)"
403 ~params:[| Repodb.Driver.Value.text "test@example.com" |]
404 with
405 | Ok () -> Alcotest.fail "expected unique constraint violation"
406 | Error _ -> ())
407
408let test_composite_index =
409 with_db (fun conn ->
410 let _ =
411 Repodb_postgresql.exec conn
412 "CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INTEGER, status \
413 TEXT, created_at TIMESTAMP)"
414 ~params:[||]
415 in
416 match
417 Repodb_postgresql.exec conn
418 "CREATE INDEX idx_orders_user_status ON orders (user_id, status)"
419 ~params:[||]
420 with
421 | Ok () -> ()
422 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e))
423
424let test_foreign_key =
425 with_db (fun conn ->
426 let _ =
427 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS posts" ~params:[||]
428 in
429 let _ =
430 Repodb_postgresql.exec conn
431 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
432 ~params:[||]
433 in
434 let _ =
435 Repodb_postgresql.exec conn
436 "CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL \
437 REFERENCES users(id), title TEXT)"
438 ~params:[||]
439 in
440 let _ =
441 Repodb_postgresql.exec conn
442 "INSERT INTO users (id, name) VALUES ($1, $2)"
443 ~params:
444 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Alice" |]
445 in
446 (match
447 Repodb_postgresql.exec conn
448 "INSERT INTO posts (user_id, title) VALUES ($1, $2)"
449 ~params:
450 [|
451 Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "First Post";
452 |]
453 with
454 | Ok () -> ()
455 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e));
456 let _ = Repodb_postgresql.exec conn "DROP TABLE posts" ~params:[||] in
457 ())
458
459let test_foreign_key_violation =
460 with_db (fun conn ->
461 let _ =
462 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS posts" ~params:[||]
463 in
464 let _ =
465 Repodb_postgresql.exec conn
466 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
467 ~params:[||]
468 in
469 let _ =
470 Repodb_postgresql.exec conn
471 "CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL \
472 REFERENCES users(id), title TEXT)"
473 ~params:[||]
474 in
475 let result =
476 Repodb_postgresql.exec conn
477 "INSERT INTO posts (user_id, title) VALUES ($1, $2)"
478 ~params:
479 [|
480 Repodb.Driver.Value.int 999;
481 Repodb.Driver.Value.text "Orphan Post";
482 |]
483 in
484 let _ = Repodb_postgresql.exec conn "DROP TABLE posts" ~params:[||] in
485 match result with
486 | Ok () -> Alcotest.fail "expected foreign key violation"
487 | Error _ -> ())
488
489let test_cascade_delete =
490 with_db (fun conn ->
491 let _ =
492 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS posts" ~params:[||]
493 in
494 let _ =
495 Repodb_postgresql.exec conn
496 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
497 ~params:[||]
498 in
499 let _ =
500 Repodb_postgresql.exec conn
501 "CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL \
502 REFERENCES users(id) ON DELETE CASCADE, title TEXT)"
503 ~params:[||]
504 in
505 let _ =
506 Repodb_postgresql.exec conn
507 "INSERT INTO users (id, name) VALUES ($1, $2)"
508 ~params:
509 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Alice" |]
510 in
511 let _ =
512 Repodb_postgresql.exec conn
513 "INSERT INTO posts (user_id, title) VALUES ($1, $2)"
514 ~params:
515 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Post 1" |]
516 in
517 let _ =
518 Repodb_postgresql.exec conn
519 "INSERT INTO posts (user_id, title) VALUES ($1, $2)"
520 ~params:
521 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Post 2" |]
522 in
523 let _ =
524 Repodb_postgresql.exec conn "DELETE FROM users WHERE id = $1"
525 ~params:[| Repodb.Driver.Value.int 1 |]
526 in
527 let result =
528 Repodb_postgresql.query conn "SELECT * FROM posts" ~params:[||]
529 in
530 let _ = Repodb_postgresql.exec conn "DROP TABLE posts" ~params:[||] in
531 match result with
532 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
533 | Ok rows -> Alcotest.(check int) "posts cascaded" 0 (List.length rows))
534
535let test_inner_join =
536 with_db (fun conn ->
537 let _ =
538 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS posts" ~params:[||]
539 in
540 let _ =
541 Repodb_postgresql.exec conn
542 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
543 ~params:[||]
544 in
545 let _ =
546 Repodb_postgresql.exec conn
547 "CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER, title \
548 TEXT)"
549 ~params:[||]
550 in
551 let _ =
552 Repodb_postgresql.exec conn
553 "INSERT INTO users (id, name) VALUES ($1, $2)"
554 ~params:
555 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Alice" |]
556 in
557 let _ =
558 Repodb_postgresql.exec conn
559 "INSERT INTO users (id, name) VALUES ($1, $2)"
560 ~params:
561 [| Repodb.Driver.Value.int 2; Repodb.Driver.Value.text "Bob" |]
562 in
563 let _ =
564 Repodb_postgresql.exec conn
565 "INSERT INTO posts (user_id, title) VALUES ($1, $2)"
566 ~params:
567 [|
568 Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Alice Post";
569 |]
570 in
571 let result =
572 Repodb_postgresql.query conn
573 "SELECT users.name, posts.title FROM users INNER JOIN posts ON \
574 users.id = posts.user_id"
575 ~params:[||]
576 in
577 let _ = Repodb_postgresql.exec conn "DROP TABLE posts" ~params:[||] in
578 match result with
579 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
580 | Ok rows ->
581 Alcotest.(check int) "one joined row" 1 (List.length rows);
582 let row = List.hd rows in
583 Alcotest.(check string) "name" "Alice" (Repodb.Driver.row_text row 0);
584 Alcotest.(check string)
585 "title" "Alice Post"
586 (Repodb.Driver.row_text row 1))
587
588let test_left_join =
589 with_db (fun conn ->
590 let _ =
591 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS posts" ~params:[||]
592 in
593 let _ =
594 Repodb_postgresql.exec conn
595 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
596 ~params:[||]
597 in
598 let _ =
599 Repodb_postgresql.exec conn
600 "CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER, title \
601 TEXT)"
602 ~params:[||]
603 in
604 let _ =
605 Repodb_postgresql.exec conn
606 "INSERT INTO users (id, name) VALUES ($1, $2)"
607 ~params:
608 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Alice" |]
609 in
610 let _ =
611 Repodb_postgresql.exec conn
612 "INSERT INTO users (id, name) VALUES ($1, $2)"
613 ~params:
614 [| Repodb.Driver.Value.int 2; Repodb.Driver.Value.text "Bob" |]
615 in
616 let _ =
617 Repodb_postgresql.exec conn
618 "INSERT INTO posts (user_id, title) VALUES ($1, $2)"
619 ~params:
620 [|
621 Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Alice Post";
622 |]
623 in
624 let result =
625 Repodb_postgresql.query conn
626 "SELECT users.name, posts.title FROM users LEFT JOIN posts ON \
627 users.id = posts.user_id ORDER BY users.id"
628 ~params:[||]
629 in
630 let _ = Repodb_postgresql.exec conn "DROP TABLE posts" ~params:[||] in
631 match result with
632 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
633 | Ok rows ->
634 Alcotest.(check int) "two rows" 2 (List.length rows);
635 let row2 = List.nth rows 1 in
636 Alcotest.(check string) "bob" "Bob" (Repodb.Driver.row_text row2 0);
637 Alcotest.(check bool)
638 "bob has no post" true
639 (Repodb.Driver.row_is_null row2 1))
640
641let test_many_to_many =
642 with_db (fun conn ->
643 let _ =
644 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS user_roles"
645 ~params:[||]
646 in
647 let _ =
648 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS roles" ~params:[||]
649 in
650 let _ =
651 Repodb_postgresql.exec conn
652 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
653 ~params:[||]
654 in
655 let _ =
656 Repodb_postgresql.exec conn
657 "CREATE TABLE roles (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
658 ~params:[||]
659 in
660 let _ =
661 Repodb_postgresql.exec conn
662 "CREATE TABLE user_roles (user_id INTEGER REFERENCES users(id), \
663 role_id INTEGER REFERENCES roles(id), PRIMARY KEY (user_id, \
664 role_id))"
665 ~params:[||]
666 in
667 let _ =
668 Repodb_postgresql.exec conn
669 "INSERT INTO users (id, name) VALUES ($1, $2)"
670 ~params:
671 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Alice" |]
672 in
673 let _ =
674 Repodb_postgresql.exec conn
675 "INSERT INTO roles (id, name) VALUES ($1, $2)"
676 ~params:
677 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "admin" |]
678 in
679 let _ =
680 Repodb_postgresql.exec conn
681 "INSERT INTO roles (id, name) VALUES ($1, $2)"
682 ~params:
683 [| Repodb.Driver.Value.int 2; Repodb.Driver.Value.text "editor" |]
684 in
685 let _ =
686 Repodb_postgresql.exec conn
687 "INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)"
688 ~params:[| Repodb.Driver.Value.int 1; Repodb.Driver.Value.int 1 |]
689 in
690 let _ =
691 Repodb_postgresql.exec conn
692 "INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)"
693 ~params:[| Repodb.Driver.Value.int 1; Repodb.Driver.Value.int 2 |]
694 in
695 let result =
696 Repodb_postgresql.query conn
697 "SELECT r.name FROM roles r INNER JOIN user_roles ur ON r.id = \
698 ur.role_id WHERE ur.user_id = $1 ORDER BY r.name"
699 ~params:[| Repodb.Driver.Value.int 1 |]
700 in
701 let _ =
702 Repodb_postgresql.exec conn "DROP TABLE user_roles" ~params:[||]
703 in
704 let _ = Repodb_postgresql.exec conn "DROP TABLE roles" ~params:[||] in
705 match result with
706 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
707 | Ok rows ->
708 Alcotest.(check int) "two roles" 2 (List.length rows);
709 Alcotest.(check string)
710 "first role" "admin"
711 (Repodb.Driver.row_text (List.nth rows 0) 0);
712 Alcotest.(check string)
713 "second role" "editor"
714 (Repodb.Driver.row_text (List.nth rows 1) 0))
715
716let test_aggregate_count =
717 with_db (fun conn ->
718 let _ =
719 Repodb_postgresql.exec conn
720 "CREATE TABLE users (id SERIAL PRIMARY KEY, department TEXT)"
721 ~params:[||]
722 in
723 let _ =
724 Repodb_postgresql.exec conn "INSERT INTO users (department) VALUES ($1)"
725 ~params:[| Repodb.Driver.Value.text "engineering" |]
726 in
727 let _ =
728 Repodb_postgresql.exec conn "INSERT INTO users (department) VALUES ($1)"
729 ~params:[| Repodb.Driver.Value.text "engineering" |]
730 in
731 let _ =
732 Repodb_postgresql.exec conn "INSERT INTO users (department) VALUES ($1)"
733 ~params:[| Repodb.Driver.Value.text "sales" |]
734 in
735 match
736 Repodb_postgresql.query conn
737 "SELECT department, COUNT(*) as cnt FROM users GROUP BY department \
738 ORDER BY cnt DESC"
739 ~params:[||]
740 with
741 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
742 | Ok rows ->
743 Alcotest.(check int) "two departments" 2 (List.length rows);
744 let row = List.hd rows in
745 Alcotest.(check string)
746 "top dept" "engineering"
747 (Repodb.Driver.row_text row 0);
748 Alcotest.(check int) "count" 2 (Repodb.Driver.row_int row 1))
749
750let test_aggregate_sum_avg =
751 with_db (fun conn ->
752 let _ =
753 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS orders" ~params:[||]
754 in
755 let _ =
756 Repodb_postgresql.exec conn
757 "CREATE TABLE orders (id SERIAL PRIMARY KEY, amount DOUBLE PRECISION)"
758 ~params:[||]
759 in
760 let _ =
761 Repodb_postgresql.exec conn "INSERT INTO orders (amount) VALUES ($1)"
762 ~params:[| Repodb.Driver.Value.float 100.0 |]
763 in
764 let _ =
765 Repodb_postgresql.exec conn "INSERT INTO orders (amount) VALUES ($1)"
766 ~params:[| Repodb.Driver.Value.float 200.0 |]
767 in
768 let _ =
769 Repodb_postgresql.exec conn "INSERT INTO orders (amount) VALUES ($1)"
770 ~params:[| Repodb.Driver.Value.float 300.0 |]
771 in
772 let result =
773 Repodb_postgresql.query_one conn
774 "SELECT SUM(amount) as total, AVG(amount) as avg FROM orders"
775 ~params:[||]
776 in
777 let _ = Repodb_postgresql.exec conn "DROP TABLE orders" ~params:[||] in
778 match result with
779 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
780 | Ok None -> Alcotest.fail "expected row"
781 | Ok (Some row) ->
782 Alcotest.(check bool)
783 "sum" true
784 (abs_float (Repodb.Driver.row_float row 0 -. 600.0) < 0.01);
785 Alcotest.(check bool)
786 "avg" true
787 (abs_float (Repodb.Driver.row_float row 1 -. 200.0) < 0.01))
788
789let test_subquery =
790 with_db (fun conn ->
791 let _ =
792 Repodb_postgresql.exec conn
793 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, score INTEGER)"
794 ~params:[||]
795 in
796 let _ =
797 Repodb_postgresql.exec conn
798 "INSERT INTO users (name, score) VALUES ($1, $2)"
799 ~params:
800 [| Repodb.Driver.Value.text "Alice"; Repodb.Driver.Value.int 90 |]
801 in
802 let _ =
803 Repodb_postgresql.exec conn
804 "INSERT INTO users (name, score) VALUES ($1, $2)"
805 ~params:
806 [| Repodb.Driver.Value.text "Bob"; Repodb.Driver.Value.int 50 |]
807 in
808 let _ =
809 Repodb_postgresql.exec conn
810 "INSERT INTO users (name, score) VALUES ($1, $2)"
811 ~params:
812 [| Repodb.Driver.Value.text "Carol"; Repodb.Driver.Value.int 80 |]
813 in
814 match
815 Repodb_postgresql.query conn
816 "SELECT name FROM users WHERE score > (SELECT AVG(score) FROM users) \
817 ORDER BY name"
818 ~params:[||]
819 with
820 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
821 | Ok rows ->
822 Alcotest.(check int) "above avg" 2 (List.length rows);
823 Alcotest.(check string)
824 "first" "Alice"
825 (Repodb.Driver.row_text (List.hd rows) 0);
826 Alcotest.(check string)
827 "second" "Carol"
828 (Repodb.Driver.row_text (List.nth rows 1) 0))
829
830let test_order_by_multiple =
831 with_db (fun conn ->
832 let _ =
833 Repodb_postgresql.exec conn
834 "CREATE TABLE users (id SERIAL PRIMARY KEY, dept TEXT, name TEXT)"
835 ~params:[||]
836 in
837 let _ =
838 Repodb_postgresql.exec conn
839 "INSERT INTO users (dept, name) VALUES ($1, $2)"
840 ~params:
841 [| Repodb.Driver.Value.text "eng"; Repodb.Driver.Value.text "Zoe" |]
842 in
843 let _ =
844 Repodb_postgresql.exec conn
845 "INSERT INTO users (dept, name) VALUES ($1, $2)"
846 ~params:
847 [|
848 Repodb.Driver.Value.text "eng"; Repodb.Driver.Value.text "Alice";
849 |]
850 in
851 let _ =
852 Repodb_postgresql.exec conn
853 "INSERT INTO users (dept, name) VALUES ($1, $2)"
854 ~params:
855 [|
856 Repodb.Driver.Value.text "sales"; Repodb.Driver.Value.text "Bob";
857 |]
858 in
859 match
860 Repodb_postgresql.query conn
861 "SELECT dept, name FROM users ORDER BY dept ASC, name ASC" ~params:[||]
862 with
863 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
864 | Ok rows ->
865 Alcotest.(check int) "three rows" 3 (List.length rows);
866 Alcotest.(check string)
867 "first" "Alice"
868 (Repodb.Driver.row_text (List.hd rows) 1);
869 Alcotest.(check string)
870 "second" "Zoe"
871 (Repodb.Driver.row_text (List.nth rows 1) 1);
872 Alcotest.(check string)
873 "third" "Bob"
874 (Repodb.Driver.row_text (List.nth rows 2) 1))
875
876let test_limit_offset =
877 with_db (fun conn ->
878 let _ =
879 Repodb_postgresql.exec conn
880 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
881 in
882 for i = 1 to 10 do
883 let _ =
884 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
885 ~params:[| Repodb.Driver.Value.int i |]
886 in
887 ()
888 done;
889 match
890 Repodb_postgresql.query conn
891 "SELECT val FROM nums ORDER BY val LIMIT 3 OFFSET 2" ~params:[||]
892 with
893 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
894 | Ok rows ->
895 Alcotest.(check int) "3 rows" 3 (List.length rows);
896 Alcotest.(check int)
897 "first val" 3
898 (Repodb.Driver.row_int (List.hd rows) 0);
899 Alcotest.(check int)
900 "last val" 5
901 (Repodb.Driver.row_int (List.nth rows 2) 0))
902
903let test_upsert =
904 with_db (fun conn ->
905 let _ =
906 Repodb_postgresql.exec conn
907 "CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT)" ~params:[||]
908 in
909 let _ =
910 Repodb_postgresql.exec conn
911 "INSERT INTO kv (key, value) VALUES ($1, $2) ON CONFLICT (key) DO \
912 UPDATE SET value = EXCLUDED.value"
913 ~params:
914 [| Repodb.Driver.Value.text "foo"; Repodb.Driver.Value.text "bar" |]
915 in
916 let _ =
917 Repodb_postgresql.exec conn
918 "INSERT INTO kv (key, value) VALUES ($1, $2) ON CONFLICT (key) DO \
919 UPDATE SET value = EXCLUDED.value"
920 ~params:
921 [| Repodb.Driver.Value.text "foo"; Repodb.Driver.Value.text "baz" |]
922 in
923 match
924 Repodb_postgresql.query_one conn "SELECT value FROM kv WHERE key = $1"
925 ~params:[| Repodb.Driver.Value.text "foo" |]
926 with
927 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
928 | Ok None -> Alcotest.fail "expected row"
929 | Ok (Some row) ->
930 Alcotest.(check string)
931 "upserted" "baz"
932 (Repodb.Driver.row_text row 0))
933
934let test_like_pattern =
935 with_db (fun conn ->
936 let _ =
937 Repodb_postgresql.exec conn
938 "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT)" ~params:[||]
939 in
940 let _ =
941 Repodb_postgresql.exec conn "INSERT INTO users (email) VALUES ($1)"
942 ~params:[| Repodb.Driver.Value.text "alice@gmail.com" |]
943 in
944 let _ =
945 Repodb_postgresql.exec conn "INSERT INTO users (email) VALUES ($1)"
946 ~params:[| Repodb.Driver.Value.text "bob@yahoo.com" |]
947 in
948 let _ =
949 Repodb_postgresql.exec conn "INSERT INTO users (email) VALUES ($1)"
950 ~params:[| Repodb.Driver.Value.text "carol@gmail.com" |]
951 in
952 match
953 Repodb_postgresql.query conn
954 "SELECT email FROM users WHERE email LIKE $1"
955 ~params:[| Repodb.Driver.Value.text "%@gmail.com" |]
956 with
957 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
958 | Ok rows -> Alcotest.(check int) "gmail users" 2 (List.length rows))
959
960let test_ilike_pattern =
961 with_db (fun conn ->
962 let _ =
963 Repodb_postgresql.exec conn
964 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
965 in
966 let _ =
967 Repodb_postgresql.exec conn "INSERT INTO users (name) VALUES ($1)"
968 ~params:[| Repodb.Driver.Value.text "Alice" |]
969 in
970 let _ =
971 Repodb_postgresql.exec conn "INSERT INTO users (name) VALUES ($1)"
972 ~params:[| Repodb.Driver.Value.text "ALICE" |]
973 in
974 let _ =
975 Repodb_postgresql.exec conn "INSERT INTO users (name) VALUES ($1)"
976 ~params:[| Repodb.Driver.Value.text "Bob" |]
977 in
978 match
979 Repodb_postgresql.query conn
980 "SELECT name FROM users WHERE name ILIKE $1"
981 ~params:[| Repodb.Driver.Value.text "alice" |]
982 with
983 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
984 | Ok rows -> Alcotest.(check int) "case insensitive" 2 (List.length rows))
985
986let test_in_clause =
987 with_db (fun conn ->
988 let _ =
989 Repodb_postgresql.exec conn
990 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
991 in
992 let _ =
993 Repodb_postgresql.exec conn
994 "INSERT INTO users (id, name) VALUES ($1, $2)"
995 ~params:
996 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Alice" |]
997 in
998 let _ =
999 Repodb_postgresql.exec conn
1000 "INSERT INTO users (id, name) VALUES ($1, $2)"
1001 ~params:
1002 [| Repodb.Driver.Value.int 2; Repodb.Driver.Value.text "Bob" |]
1003 in
1004 let _ =
1005 Repodb_postgresql.exec conn
1006 "INSERT INTO users (id, name) VALUES ($1, $2)"
1007 ~params:
1008 [| Repodb.Driver.Value.int 3; Repodb.Driver.Value.text "Carol" |]
1009 in
1010 match
1011 Repodb_postgresql.query conn
1012 "SELECT name FROM users WHERE id IN ($1, $2) ORDER BY name"
1013 ~params:[| Repodb.Driver.Value.int 1; Repodb.Driver.Value.int 3 |]
1014 with
1015 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1016 | Ok rows ->
1017 Alcotest.(check int) "two users" 2 (List.length rows);
1018 Alcotest.(check string)
1019 "first" "Alice"
1020 (Repodb.Driver.row_text (List.hd rows) 0);
1021 Alcotest.(check string)
1022 "second" "Carol"
1023 (Repodb.Driver.row_text (List.nth rows 1) 0))
1024
1025let test_between =
1026 with_db (fun conn ->
1027 let _ =
1028 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS products" ~params:[||]
1029 in
1030 let _ =
1031 Repodb_postgresql.exec conn
1032 "CREATE TABLE products (id SERIAL PRIMARY KEY, price DOUBLE \
1033 PRECISION)"
1034 ~params:[||]
1035 in
1036 let _ =
1037 Repodb_postgresql.exec conn "INSERT INTO products (price) VALUES ($1)"
1038 ~params:[| Repodb.Driver.Value.float 10.0 |]
1039 in
1040 let _ =
1041 Repodb_postgresql.exec conn "INSERT INTO products (price) VALUES ($1)"
1042 ~params:[| Repodb.Driver.Value.float 25.0 |]
1043 in
1044 let _ =
1045 Repodb_postgresql.exec conn "INSERT INTO products (price) VALUES ($1)"
1046 ~params:[| Repodb.Driver.Value.float 50.0 |]
1047 in
1048 let result =
1049 Repodb_postgresql.query conn
1050 "SELECT price FROM products WHERE price BETWEEN $1 AND $2"
1051 ~params:
1052 [| Repodb.Driver.Value.float 15.0; Repodb.Driver.Value.float 30.0 |]
1053 in
1054 let _ = Repodb_postgresql.exec conn "DROP TABLE products" ~params:[||] in
1055 match result with
1056 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1057 | Ok rows -> Alcotest.(check int) "one product" 1 (List.length rows))
1058
1059let test_coalesce =
1060 with_db (fun conn ->
1061 let _ =
1062 Repodb_postgresql.exec conn
1063 "CREATE TABLE users (id SERIAL PRIMARY KEY, nickname TEXT, name TEXT)"
1064 ~params:[||]
1065 in
1066 let _ =
1067 Repodb_postgresql.exec conn
1068 "INSERT INTO users (nickname, name) VALUES ($1, $2)"
1069 ~params:
1070 [|
1071 Repodb.Driver.Value.text "Al"; Repodb.Driver.Value.text "Alice";
1072 |]
1073 in
1074 let _ =
1075 Repodb_postgresql.exec conn
1076 "INSERT INTO users (nickname, name) VALUES (NULL, $1)"
1077 ~params:[| Repodb.Driver.Value.text "Bob" |]
1078 in
1079 match
1080 Repodb_postgresql.query conn
1081 "SELECT COALESCE(nickname, name) as display FROM users ORDER BY id"
1082 ~params:[||]
1083 with
1084 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1085 | Ok rows ->
1086 Alcotest.(check int) "two rows" 2 (List.length rows);
1087 Alcotest.(check string)
1088 "first" "Al"
1089 (Repodb.Driver.row_text (List.hd rows) 0);
1090 Alcotest.(check string)
1091 "second" "Bob"
1092 (Repodb.Driver.row_text (List.nth rows 1) 0))
1093
1094let test_case_when =
1095 with_db (fun conn ->
1096 let _ =
1097 Repodb_postgresql.exec conn
1098 "CREATE TABLE users (id SERIAL PRIMARY KEY, score INTEGER)"
1099 ~params:[||]
1100 in
1101 let _ =
1102 Repodb_postgresql.exec conn "INSERT INTO users (score) VALUES ($1)"
1103 ~params:[| Repodb.Driver.Value.int 90 |]
1104 in
1105 let _ =
1106 Repodb_postgresql.exec conn "INSERT INTO users (score) VALUES ($1)"
1107 ~params:[| Repodb.Driver.Value.int 75 |]
1108 in
1109 let _ =
1110 Repodb_postgresql.exec conn "INSERT INTO users (score) VALUES ($1)"
1111 ~params:[| Repodb.Driver.Value.int 50 |]
1112 in
1113 match
1114 Repodb_postgresql.query conn
1115 "SELECT score, CASE WHEN score >= 80 THEN 'A' WHEN score >= 60 THEN \
1116 'B' ELSE 'C' END as grade FROM users ORDER BY score DESC"
1117 ~params:[||]
1118 with
1119 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1120 | Ok rows ->
1121 Alcotest.(check int) "three rows" 3 (List.length rows);
1122 Alcotest.(check string)
1123 "first grade" "A"
1124 (Repodb.Driver.row_text (List.hd rows) 1);
1125 Alcotest.(check string)
1126 "second grade" "B"
1127 (Repodb.Driver.row_text (List.nth rows 1) 1);
1128 Alcotest.(check string)
1129 "third grade" "C"
1130 (Repodb.Driver.row_text (List.nth rows 2) 1))
1131
1132let test_having =
1133 with_db (fun conn ->
1134 let _ =
1135 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS sales" ~params:[||]
1136 in
1137 let _ =
1138 Repodb_postgresql.exec conn
1139 "CREATE TABLE sales (id SERIAL PRIMARY KEY, product TEXT, amount \
1140 INTEGER)"
1141 ~params:[||]
1142 in
1143 let _ =
1144 Repodb_postgresql.exec conn
1145 "INSERT INTO sales (product, amount) VALUES ($1, $2)"
1146 ~params:
1147 [| Repodb.Driver.Value.text "A"; Repodb.Driver.Value.int 100 |]
1148 in
1149 let _ =
1150 Repodb_postgresql.exec conn
1151 "INSERT INTO sales (product, amount) VALUES ($1, $2)"
1152 ~params:
1153 [| Repodb.Driver.Value.text "A"; Repodb.Driver.Value.int 200 |]
1154 in
1155 let _ =
1156 Repodb_postgresql.exec conn
1157 "INSERT INTO sales (product, amount) VALUES ($1, $2)"
1158 ~params:[| Repodb.Driver.Value.text "B"; Repodb.Driver.Value.int 50 |]
1159 in
1160 let result =
1161 Repodb_postgresql.query conn
1162 "SELECT product, SUM(amount) as total FROM sales GROUP BY product \
1163 HAVING SUM(amount) > $1"
1164 ~params:[| Repodb.Driver.Value.int 100 |]
1165 in
1166 let _ = Repodb_postgresql.exec conn "DROP TABLE sales" ~params:[||] in
1167 match result with
1168 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1169 | Ok rows ->
1170 Alcotest.(check int) "one product over 100" 1 (List.length rows);
1171 Alcotest.(check string)
1172 "product A" "A"
1173 (Repodb.Driver.row_text (List.hd rows) 0))
1174
1175let test_self_join =
1176 with_db (fun conn ->
1177 let _ =
1178 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS employees" ~params:[||]
1179 in
1180 let _ =
1181 Repodb_postgresql.exec conn
1182 "CREATE TABLE employees (id SERIAL PRIMARY KEY, name TEXT, \
1183 manager_id INTEGER)"
1184 ~params:[||]
1185 in
1186 let _ =
1187 Repodb_postgresql.exec conn
1188 "INSERT INTO employees (id, name, manager_id) VALUES ($1, $2, NULL)"
1189 ~params:
1190 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Boss" |]
1191 in
1192 let _ =
1193 Repodb_postgresql.exec conn
1194 "INSERT INTO employees (id, name, manager_id) VALUES ($1, $2, $3)"
1195 ~params:
1196 [|
1197 Repodb.Driver.Value.int 2;
1198 Repodb.Driver.Value.text "Alice";
1199 Repodb.Driver.Value.int 1;
1200 |]
1201 in
1202 let _ =
1203 Repodb_postgresql.exec conn
1204 "INSERT INTO employees (id, name, manager_id) VALUES ($1, $2, $3)"
1205 ~params:
1206 [|
1207 Repodb.Driver.Value.int 3;
1208 Repodb.Driver.Value.text "Bob";
1209 Repodb.Driver.Value.int 1;
1210 |]
1211 in
1212 let result =
1213 Repodb_postgresql.query conn
1214 "SELECT e.name, m.name as manager FROM employees e LEFT JOIN \
1215 employees m ON e.manager_id = m.id ORDER BY e.id"
1216 ~params:[||]
1217 in
1218 let _ = Repodb_postgresql.exec conn "DROP TABLE employees" ~params:[||] in
1219 match result with
1220 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1221 | Ok rows ->
1222 Alcotest.(check int) "three employees" 3 (List.length rows);
1223 Alcotest.(check bool)
1224 "boss has no manager" true
1225 (Repodb.Driver.row_is_null (List.hd rows) 1);
1226 Alcotest.(check string)
1227 "alice manager" "Boss"
1228 (Repodb.Driver.row_text (List.nth rows 1) 1))
1229
1230let test_array_type =
1231 with_db (fun conn ->
1232 let _ =
1233 Repodb_postgresql.exec conn
1234 "CREATE TABLE users (id SERIAL PRIMARY KEY, tags TEXT[])" ~params:[||]
1235 in
1236 let _ =
1237 Repodb_postgresql.exec conn "INSERT INTO users (tags) VALUES ($1)"
1238 ~params:[| Repodb.Driver.Value.text "{\"a\",\"b\",\"c\"}" |]
1239 in
1240 match
1241 Repodb_postgresql.query_one conn "SELECT tags FROM users" ~params:[||]
1242 with
1243 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1244 | Ok None -> Alcotest.fail "expected row"
1245 | Ok (Some row) ->
1246 let tags = Repodb.Driver.row_text row 0 in
1247 Alcotest.(check bool) "has a" true (String.length tags > 0))
1248
1249let test_jsonb_type =
1250 with_db (fun conn ->
1251 let _ =
1252 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS docs" ~params:[||]
1253 in
1254 let _ =
1255 Repodb_postgresql.exec conn
1256 "CREATE TABLE docs (id SERIAL PRIMARY KEY, data JSONB)" ~params:[||]
1257 in
1258 let _ =
1259 Repodb_postgresql.exec conn "INSERT INTO docs (data) VALUES ($1::jsonb)"
1260 ~params:
1261 [| Repodb.Driver.Value.text "{\"name\": \"test\", \"value\": 42}" |]
1262 in
1263 let result =
1264 Repodb_postgresql.query_one conn
1265 "SELECT data->>'name' as name, (data->>'value')::int as val FROM docs"
1266 ~params:[||]
1267 in
1268 let _ = Repodb_postgresql.exec conn "DROP TABLE docs" ~params:[||] in
1269 match result with
1270 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1271 | Ok None -> Alcotest.fail "expected row"
1272 | Ok (Some row) ->
1273 Alcotest.(check string) "name" "test" (Repodb.Driver.row_text row 0);
1274 Alcotest.(check int) "value" 42 (Repodb.Driver.row_int row 1))
1275
1276let test_cte =
1277 with_db (fun conn ->
1278 let _ =
1279 Repodb_postgresql.exec conn
1280 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, score INTEGER)"
1281 ~params:[||]
1282 in
1283 let _ =
1284 Repodb_postgresql.exec conn
1285 "INSERT INTO users (name, score) VALUES ($1, $2)"
1286 ~params:
1287 [| Repodb.Driver.Value.text "Alice"; Repodb.Driver.Value.int 100 |]
1288 in
1289 let _ =
1290 Repodb_postgresql.exec conn
1291 "INSERT INTO users (name, score) VALUES ($1, $2)"
1292 ~params:
1293 [| Repodb.Driver.Value.text "Bob"; Repodb.Driver.Value.int 80 |]
1294 in
1295 match
1296 Repodb_postgresql.query conn
1297 "WITH high_scorers AS (SELECT name, score FROM users WHERE score > \
1298 90) SELECT name FROM high_scorers"
1299 ~params:[||]
1300 with
1301 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1302 | Ok rows ->
1303 Alcotest.(check int) "one high scorer" 1 (List.length rows);
1304 Alcotest.(check string)
1305 "alice" "Alice"
1306 (Repodb.Driver.row_text (List.hd rows) 0))
1307
1308let test_window_function =
1309 with_db (fun conn ->
1310 let _ =
1311 Repodb_postgresql.exec conn
1312 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, dept TEXT, \
1313 salary INTEGER)"
1314 ~params:[||]
1315 in
1316 let _ =
1317 Repodb_postgresql.exec conn
1318 "INSERT INTO users (name, dept, salary) VALUES ($1, $2, $3)"
1319 ~params:
1320 [|
1321 Repodb.Driver.Value.text "Alice";
1322 Repodb.Driver.Value.text "eng";
1323 Repodb.Driver.Value.int 100;
1324 |]
1325 in
1326 let _ =
1327 Repodb_postgresql.exec conn
1328 "INSERT INTO users (name, dept, salary) VALUES ($1, $2, $3)"
1329 ~params:
1330 [|
1331 Repodb.Driver.Value.text "Bob";
1332 Repodb.Driver.Value.text "eng";
1333 Repodb.Driver.Value.int 90;
1334 |]
1335 in
1336 let _ =
1337 Repodb_postgresql.exec conn
1338 "INSERT INTO users (name, dept, salary) VALUES ($1, $2, $3)"
1339 ~params:
1340 [|
1341 Repodb.Driver.Value.text "Carol";
1342 Repodb.Driver.Value.text "sales";
1343 Repodb.Driver.Value.int 80;
1344 |]
1345 in
1346 match
1347 Repodb_postgresql.query conn
1348 "SELECT name, salary, RANK() OVER (PARTITION BY dept ORDER BY salary \
1349 DESC) as rank FROM users ORDER BY dept, rank"
1350 ~params:[||]
1351 with
1352 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1353 | Ok rows ->
1354 Alcotest.(check int) "three rows" 3 (List.length rows);
1355 Alcotest.(check string)
1356 "alice first in eng" "Alice"
1357 (Repodb.Driver.row_text (List.hd rows) 0);
1358 Alcotest.(check int)
1359 "alice rank 1" 1
1360 (Repodb.Driver.row_int (List.hd rows) 2))
1361
1362let basic_tests =
1363 [
1364 ("connect", `Quick, test_connect);
1365 ("create_table", `Quick, test_create_table);
1366 ("insert_and_query", `Quick, test_insert_and_query);
1367 ("query_one", `Quick, test_query_one);
1368 ("query_one_not_found", `Quick, test_query_one_not_found);
1369 ("update", `Quick, test_update);
1370 ("delete", `Quick, test_delete);
1371 ("transaction_commit", `Quick, test_transaction_commit);
1372 ("transaction_rollback", `Quick, test_transaction_rollback);
1373 ("query_fold", `Quick, test_query_fold);
1374 ("query_iter", `Quick, test_query_iter);
1375 ("returning", `Quick, test_returning);
1376 ("null_handling", `Quick, test_null_handling);
1377 ("bool_values", `Quick, test_bool_values);
1378 ("float_values", `Quick, test_float_values);
1379 ("with_connection", `Quick, test_with_connection);
1380 ("placeholder", `Quick, test_placeholder);
1381 ]
1382
1383let index_tests =
1384 [
1385 ("create_index", `Quick, test_create_index);
1386 ("create_unique_index", `Quick, test_create_unique_index);
1387 ("composite_index", `Quick, test_composite_index);
1388 ]
1389
1390let relation_tests =
1391 [
1392 ("foreign_key", `Quick, test_foreign_key);
1393 ("foreign_key_violation", `Quick, test_foreign_key_violation);
1394 ("cascade_delete", `Quick, test_cascade_delete);
1395 ("many_to_many", `Quick, test_many_to_many);
1396 ]
1397
1398let join_tests =
1399 [
1400 ("inner_join", `Quick, test_inner_join);
1401 ("left_join", `Quick, test_left_join);
1402 ("self_join", `Quick, test_self_join);
1403 ]
1404
1405let query_tests =
1406 [
1407 ("aggregate_count", `Quick, test_aggregate_count);
1408 ("aggregate_sum_avg", `Quick, test_aggregate_sum_avg);
1409 ("subquery", `Quick, test_subquery);
1410 ("order_by_multiple", `Quick, test_order_by_multiple);
1411 ("limit_offset", `Quick, test_limit_offset);
1412 ("upsert", `Quick, test_upsert);
1413 ("like_pattern", `Quick, test_like_pattern);
1414 ("ilike_pattern", `Quick, test_ilike_pattern);
1415 ("in_clause", `Quick, test_in_clause);
1416 ("between", `Quick, test_between);
1417 ("coalesce", `Quick, test_coalesce);
1418 ("case_when", `Quick, test_case_when);
1419 ("having", `Quick, test_having);
1420 ]
1421
1422let advanced_tests =
1423 [
1424 ("array_type", `Quick, test_array_type);
1425 ("jsonb_type", `Quick, test_jsonb_type);
1426 ("cte", `Quick, test_cte);
1427 ("window_function", `Quick, test_window_function);
1428 ]
1429
1430let test_fold_sum_large =
1431 with_db (fun conn ->
1432 let _ =
1433 Repodb_postgresql.exec conn
1434 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
1435 in
1436 for i = 1 to 1000 do
1437 let _ =
1438 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
1439 ~params:[| Repodb.Driver.Value.int i |]
1440 in
1441 ()
1442 done;
1443 match
1444 Repodb_postgresql.query_fold conn "SELECT val FROM nums" ~params:[||]
1445 ~init:0 ~f:(fun acc row -> acc + Repodb.Driver.row_int row 0)
1446 with
1447 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1448 | Ok sum -> Alcotest.(check int) "sum 1..1000" 500500 sum)
1449
1450let test_fold_collect_strings =
1451 with_db (fun conn ->
1452 let _ =
1453 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS words" ~params:[||]
1454 in
1455 let _ =
1456 Repodb_postgresql.exec conn
1457 "CREATE TABLE words (id SERIAL PRIMARY KEY, word TEXT)" ~params:[||]
1458 in
1459 let words = [ "apple"; "banana"; "cherry"; "date"; "elderberry" ] in
1460 List.iter
1461 (fun w ->
1462 let _ =
1463 Repodb_postgresql.exec conn "INSERT INTO words (word) VALUES ($1)"
1464 ~params:[| Repodb.Driver.Value.text w |]
1465 in
1466 ())
1467 words;
1468 match
1469 Repodb_postgresql.query_fold conn "SELECT word FROM words ORDER BY word"
1470 ~params:[||] ~init:[] ~f:(fun acc row ->
1471 Repodb.Driver.row_text row 0 :: acc)
1472 with
1473 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1474 | Ok collected ->
1475 let collected = List.rev collected in
1476 Alcotest.(check int) "5 words" 5 (List.length collected);
1477 Alcotest.(check string) "first" "apple" (List.hd collected);
1478 Alcotest.(check string) "last" "elderberry" (List.nth collected 4))
1479
1480let test_fold_with_index =
1481 with_db (fun conn ->
1482 let _ =
1483 Repodb_postgresql.exec conn
1484 "CREATE TABLE items (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
1485 in
1486 let _ =
1487 Repodb_postgresql.exec conn "INSERT INTO items (name) VALUES ($1)"
1488 ~params:[| Repodb.Driver.Value.text "first" |]
1489 in
1490 let _ =
1491 Repodb_postgresql.exec conn "INSERT INTO items (name) VALUES ($1)"
1492 ~params:[| Repodb.Driver.Value.text "second" |]
1493 in
1494 let _ =
1495 Repodb_postgresql.exec conn "INSERT INTO items (name) VALUES ($1)"
1496 ~params:[| Repodb.Driver.Value.text "third" |]
1497 in
1498 match
1499 Repodb_postgresql.query_fold conn "SELECT name FROM items ORDER BY id"
1500 ~params:[||] ~init:(0, []) ~f:(fun (idx, acc) row ->
1501 let name = Repodb.Driver.row_text row 0 in
1502 (idx + 1, (idx, name) :: acc))
1503 with
1504 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1505 | Ok (count, pairs) ->
1506 Alcotest.(check int) "count" 3 count;
1507 let pairs = List.rev pairs in
1508 Alcotest.(check (pair int string))
1509 "first" (0, "first") (List.hd pairs);
1510 Alcotest.(check (pair int string))
1511 "last" (2, "third") (List.nth pairs 2))
1512
1513let test_fold_min_max =
1514 with_db (fun conn ->
1515 let _ =
1516 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS prices" ~params:[||]
1517 in
1518 let _ =
1519 Repodb_postgresql.exec conn
1520 "CREATE TABLE prices (id SERIAL PRIMARY KEY, price DOUBLE PRECISION)"
1521 ~params:[||]
1522 in
1523 let prices = [ 10.5; 25.0; 5.25; 100.0; 42.0 ] in
1524 List.iter
1525 (fun p ->
1526 let _ =
1527 Repodb_postgresql.exec conn "INSERT INTO prices (price) VALUES ($1)"
1528 ~params:[| Repodb.Driver.Value.float p |]
1529 in
1530 ())
1531 prices;
1532 let result =
1533 Repodb_postgresql.query_fold conn "SELECT price FROM prices" ~params:[||]
1534 ~init:(Float.infinity, Float.neg_infinity)
1535 ~f:(fun (min_v, max_v) row ->
1536 let p = Repodb.Driver.row_float row 0 in
1537 (Float.min min_v p, Float.max max_v p))
1538 in
1539 let _ = Repodb_postgresql.exec conn "DROP TABLE prices" ~params:[||] in
1540 match result with
1541 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1542 | Ok (min_v, max_v) ->
1543 Alcotest.(check bool) "min" true (abs_float (min_v -. 5.25) < 0.001);
1544 Alcotest.(check bool) "max" true (abs_float (max_v -. 100.0) < 0.001))
1545
1546let test_fold_empty_result =
1547 with_db (fun conn ->
1548 let _ =
1549 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS empty_table"
1550 ~params:[||]
1551 in
1552 let _ =
1553 Repodb_postgresql.exec conn
1554 "CREATE TABLE empty_table (id SERIAL PRIMARY KEY, val INTEGER)"
1555 ~params:[||]
1556 in
1557 let result =
1558 Repodb_postgresql.query_fold conn "SELECT val FROM empty_table"
1559 ~params:[||] ~init:42 ~f:(fun acc row ->
1560 acc + Repodb.Driver.row_int row 0)
1561 in
1562 let _ =
1563 Repodb_postgresql.exec conn "DROP TABLE empty_table" ~params:[||]
1564 in
1565 match result with
1566 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1567 | Ok result -> Alcotest.(check int) "unchanged init" 42 result)
1568
1569let test_iter_side_effects =
1570 with_db (fun conn ->
1571 let _ =
1572 Repodb_postgresql.exec conn
1573 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
1574 in
1575 for i = 1 to 100 do
1576 let _ =
1577 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
1578 ~params:[| Repodb.Driver.Value.int i |]
1579 in
1580 ()
1581 done;
1582 let sum = ref 0 in
1583 let count = ref 0 in
1584 match
1585 Repodb_postgresql.query_iter conn "SELECT val FROM nums" ~params:[||]
1586 ~f:(fun row ->
1587 incr count;
1588 sum := !sum + Repodb.Driver.row_int row 0)
1589 with
1590 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1591 | Ok () ->
1592 Alcotest.(check int) "count" 100 !count;
1593 Alcotest.(check int) "sum" 5050 !sum)
1594
1595let test_iter_early_termination =
1596 with_db (fun conn ->
1597 let _ =
1598 Repodb_postgresql.exec conn
1599 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
1600 in
1601 for i = 1 to 10 do
1602 let _ =
1603 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
1604 ~params:[| Repodb.Driver.Value.int i |]
1605 in
1606 ()
1607 done;
1608 let processed = ref 0 in
1609 let stop_exception = ref None in
1610 let result =
1611 try
1612 Repodb_postgresql.query_iter conn "SELECT val FROM nums ORDER BY val"
1613 ~params:[||] ~f:(fun row ->
1614 incr processed;
1615 let v = Repodb.Driver.row_int row 0 in
1616 if v = 5 then begin
1617 stop_exception := Some "stop at 5";
1618 raise Exit
1619 end)
1620 with Exit -> Ok ()
1621 in
1622 match result with
1623 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1624 | Ok () ->
1625 Alcotest.(check bool) "stopped" true (Option.is_some !stop_exception);
1626 Alcotest.(check int) "processed before stop" 5 !processed)
1627
1628let test_iter_collect_to_buffer =
1629 with_db (fun conn ->
1630 let _ =
1631 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS lines" ~params:[||]
1632 in
1633 let _ =
1634 Repodb_postgresql.exec conn
1635 "CREATE TABLE lines (id SERIAL PRIMARY KEY, line TEXT)" ~params:[||]
1636 in
1637 let lines = [ "line1"; "line2"; "line3" ] in
1638 List.iter
1639 (fun l ->
1640 let _ =
1641 Repodb_postgresql.exec conn "INSERT INTO lines (line) VALUES ($1)"
1642 ~params:[| Repodb.Driver.Value.text l |]
1643 in
1644 ())
1645 lines;
1646 let buf = Buffer.create 64 in
1647 let result =
1648 Repodb_postgresql.query_iter conn "SELECT line FROM lines ORDER BY id"
1649 ~params:[||] ~f:(fun row ->
1650 Buffer.add_string buf (Repodb.Driver.row_text row 0);
1651 Buffer.add_char buf '\n')
1652 in
1653 let _ = Repodb_postgresql.exec conn "DROP TABLE lines" ~params:[||] in
1654 match result with
1655 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1656 | Ok () ->
1657 let content = Buffer.contents buf in
1658 Alcotest.(check string) "buffer" "line1\nline2\nline3\n" content)
1659
1660let test_fold_with_filter =
1661 with_db (fun conn ->
1662 let _ =
1663 Repodb_postgresql.exec conn
1664 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
1665 in
1666 for i = 1 to 20 do
1667 let _ =
1668 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
1669 ~params:[| Repodb.Driver.Value.int i |]
1670 in
1671 ()
1672 done;
1673 match
1674 Repodb_postgresql.query_fold conn "SELECT val FROM nums" ~params:[||]
1675 ~init:[] ~f:(fun acc row ->
1676 let v = Repodb.Driver.row_int row 0 in
1677 if v mod 2 = 0 then v :: acc else acc)
1678 with
1679 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1680 | Ok evens ->
1681 let evens = List.rev evens in
1682 Alcotest.(check int) "10 evens" 10 (List.length evens);
1683 Alcotest.(check int) "first even" 2 (List.hd evens);
1684 Alcotest.(check int) "last even" 20 (List.nth evens 9))
1685
1686module PgStream = Repodb.Stream.Make (Repodb_postgresql.Driver)
1687
1688let test_stream_fold_map =
1689 with_db (fun conn ->
1690 let _ =
1691 Repodb_postgresql.exec conn
1692 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
1693 in
1694 let names = [ "Alice"; "Bob"; "Carol" ] in
1695 List.iter
1696 (fun n ->
1697 let _ =
1698 Repodb_postgresql.exec conn "INSERT INTO users (name) VALUES ($1)"
1699 ~params:[| Repodb.Driver.Value.text n |]
1700 in
1701 ())
1702 names;
1703 match
1704 PgStream.fold_map conn "SELECT name FROM users ORDER BY name"
1705 ~params:[||] ~f:(fun row ->
1706 String.uppercase_ascii (Repodb.Driver.row_text row 0))
1707 with
1708 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1709 | Ok result ->
1710 Alcotest.(check int) "3 items" 3 (List.length result);
1711 Alcotest.(check string) "first" "ALICE" (List.hd result);
1712 Alcotest.(check string) "last" "CAROL" (List.nth result 2))
1713
1714let test_stream_iter =
1715 with_db (fun conn ->
1716 let _ =
1717 Repodb_postgresql.exec conn
1718 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
1719 in
1720 for i = 1 to 5 do
1721 let _ =
1722 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
1723 ~params:[| Repodb.Driver.Value.int i |]
1724 in
1725 ()
1726 done;
1727 let product = ref 1 in
1728 match
1729 PgStream.iter conn "SELECT val FROM nums" ~params:[||] ~f:(fun row ->
1730 product := !product * Repodb.Driver.row_int row 0)
1731 with
1732 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1733 | Ok () -> Alcotest.(check int) "product" 120 !product)
1734
1735let test_cursor_fold =
1736 with_db (fun conn ->
1737 let _ =
1738 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS big_nums" ~params:[||]
1739 in
1740 let _ =
1741 Repodb_postgresql.exec conn
1742 "CREATE TABLE big_nums (id SERIAL PRIMARY KEY, val INTEGER)"
1743 ~params:[||]
1744 in
1745 for i = 1 to 100 do
1746 let _ =
1747 Repodb_postgresql.exec conn "INSERT INTO big_nums (val) VALUES ($1)"
1748 ~params:[| Repodb.Driver.Value.int i |]
1749 in
1750 ()
1751 done;
1752 let config = { Repodb.Stream.batch_size = 10; max_rows = None } in
1753 let result =
1754 PgStream.cursor_fold conn ~config "SELECT val FROM big_nums"
1755 ~params:[||] ~init:0 ~f:(fun acc row ->
1756 acc + Repodb.Driver.row_int row 0)
1757 in
1758 let _ = Repodb_postgresql.exec conn "DROP TABLE big_nums" ~params:[||] in
1759 match result with
1760 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1761 | Ok sum -> Alcotest.(check int) "sum 1..100" 5050 sum)
1762
1763let test_cursor_fold_with_max_rows =
1764 with_db (fun conn ->
1765 let _ =
1766 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS big_nums" ~params:[||]
1767 in
1768 let _ =
1769 Repodb_postgresql.exec conn
1770 "CREATE TABLE big_nums (id SERIAL PRIMARY KEY, val INTEGER)"
1771 ~params:[||]
1772 in
1773 for i = 1 to 100 do
1774 let _ =
1775 Repodb_postgresql.exec conn "INSERT INTO big_nums (val) VALUES ($1)"
1776 ~params:[| Repodb.Driver.Value.int i |]
1777 in
1778 ()
1779 done;
1780 let config = { Repodb.Stream.batch_size = 10; max_rows = Some 25 } in
1781 let result =
1782 PgStream.cursor_fold conn ~config
1783 "SELECT val FROM big_nums ORDER BY val" ~params:[||] ~init:0
1784 ~f:(fun acc row -> acc + Repodb.Driver.row_int row 0)
1785 in
1786 let _ = Repodb_postgresql.exec conn "DROP TABLE big_nums" ~params:[||] in
1787 match result with
1788 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1789 | Ok sum -> Alcotest.(check int) "sum 1..25" 325 sum)
1790
1791let test_cursor_iter =
1792 with_db (fun conn ->
1793 let _ =
1794 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS big_nums" ~params:[||]
1795 in
1796 let _ =
1797 Repodb_postgresql.exec conn
1798 "CREATE TABLE big_nums (id SERIAL PRIMARY KEY, val INTEGER)"
1799 ~params:[||]
1800 in
1801 for i = 1 to 50 do
1802 let _ =
1803 Repodb_postgresql.exec conn "INSERT INTO big_nums (val) VALUES ($1)"
1804 ~params:[| Repodb.Driver.Value.int i |]
1805 in
1806 ()
1807 done;
1808 let config = { Repodb.Stream.batch_size = 7; max_rows = None } in
1809 let count = ref 0 in
1810 let sum = ref 0 in
1811 let result =
1812 PgStream.cursor_iter conn ~config "SELECT val FROM big_nums" ~params:[||]
1813 ~f:(fun row ->
1814 incr count;
1815 sum := !sum + Repodb.Driver.row_int row 0)
1816 in
1817 let _ = Repodb_postgresql.exec conn "DROP TABLE big_nums" ~params:[||] in
1818 match result with
1819 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1820 | Ok () ->
1821 Alcotest.(check int) "count" 50 !count;
1822 Alcotest.(check int) "sum" 1275 !sum)
1823
1824let test_cursor_small_batches =
1825 with_db (fun conn ->
1826 let _ =
1827 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS items" ~params:[||]
1828 in
1829 let _ =
1830 Repodb_postgresql.exec conn
1831 "CREATE TABLE items (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
1832 in
1833 for i = 1 to 23 do
1834 let _ =
1835 Repodb_postgresql.exec conn "INSERT INTO items (name) VALUES ($1)"
1836 ~params:[| Repodb.Driver.Value.text (Printf.sprintf "item%d" i) |]
1837 in
1838 ()
1839 done;
1840 let config = { Repodb.Stream.batch_size = 5; max_rows = None } in
1841 let result =
1842 PgStream.cursor_fold conn ~config "SELECT name FROM items" ~params:[||]
1843 ~init:[] ~f:(fun acc row -> Repodb.Driver.row_text row 0 :: acc)
1844 in
1845 let _ = Repodb_postgresql.exec conn "DROP TABLE items" ~params:[||] in
1846 match result with
1847 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1848 | Ok names -> Alcotest.(check int) "23 items" 23 (List.length names))
1849
1850let test_sync_operations =
1851 with_db (fun conn ->
1852 let _ =
1853 Repodb_postgresql.exec conn
1854 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
1855 in
1856 for i = 1 to 10 do
1857 let _ =
1858 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
1859 ~params:[| Repodb.Driver.Value.int i |]
1860 in
1861 ()
1862 done;
1863 match
1864 Repodb_postgresql.query conn "SELECT val FROM nums" ~params:[||]
1865 with
1866 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1867 | Ok rows ->
1868 let taken = Repodb.Stream.Sync.take 3 rows in
1869 Alcotest.(check int) "take 3" 3 (List.length taken);
1870 let dropped = Repodb.Stream.Sync.drop 7 rows in
1871 Alcotest.(check int) "drop 7" 3 (List.length dropped);
1872 let chunks = Repodb.Stream.Sync.chunks 4 rows in
1873 Alcotest.(check int) "3 chunks" 3 (List.length chunks);
1874 Alcotest.(check int)
1875 "first chunk size" 4
1876 (List.length (List.hd chunks));
1877 Alcotest.(check int)
1878 "last chunk size" 2
1879 (List.length (List.nth chunks 2));
1880 let vals =
1881 Repodb.Stream.Sync.map ~rows ~f:(fun r -> Repodb.Driver.row_int r 0)
1882 in
1883 Alcotest.(check int) "sum via map" 55 (List.fold_left ( + ) 0 vals);
1884 let evens =
1885 Repodb.Stream.Sync.filter_map ~rows ~f:(fun r ->
1886 let v = Repodb.Driver.row_int r 0 in
1887 if v mod 2 = 0 then Some v else None)
1888 in
1889 Alcotest.(check int) "5 evens" 5 (List.length evens);
1890 let found =
1891 Repodb.Stream.Sync.find ~rows ~f:(fun r ->
1892 Repodb.Driver.row_int r 0 = 5)
1893 in
1894 Alcotest.(check bool) "found 5" true (Option.is_some found);
1895 let all_positive =
1896 Repodb.Stream.Sync.for_all ~rows ~f:(fun r ->
1897 Repodb.Driver.row_int r 0 > 0)
1898 in
1899 Alcotest.(check bool) "all positive" true all_positive;
1900 let has_ten =
1901 Repodb.Stream.Sync.exists ~rows ~f:(fun r ->
1902 Repodb.Driver.row_int r 0 = 10)
1903 in
1904 Alcotest.(check bool) "has 10" true has_ten)
1905
1906let test_seq_operations =
1907 with_db (fun conn ->
1908 let _ =
1909 Repodb_postgresql.exec conn
1910 "CREATE TABLE nums (id SERIAL PRIMARY KEY, val INTEGER)" ~params:[||]
1911 in
1912 for i = 1 to 10 do
1913 let _ =
1914 Repodb_postgresql.exec conn "INSERT INTO nums (val) VALUES ($1)"
1915 ~params:[| Repodb.Driver.Value.int i |]
1916 in
1917 ()
1918 done;
1919 match
1920 Repodb_postgresql.query conn "SELECT val FROM nums" ~params:[||]
1921 with
1922 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
1923 | Ok rows ->
1924 let seq = Repodb.Stream.Seq.of_list rows in
1925 let sum =
1926 Repodb.Stream.Seq.fold ~seq ~init:0 ~f:(fun acc r ->
1927 acc + Repodb.Driver.row_int r 0)
1928 in
1929 Alcotest.(check int) "sum" 55 sum;
1930 let seq = Repodb.Stream.Seq.of_list rows in
1931 let taken = Repodb.Stream.Seq.take 3 seq in
1932 let taken_list = List.of_seq taken in
1933 Alcotest.(check int) "take 3" 3 (List.length taken_list);
1934 let seq = Repodb.Stream.Seq.of_list rows in
1935 let dropped = Repodb.Stream.Seq.drop 7 seq in
1936 let dropped_list = List.of_seq dropped in
1937 Alcotest.(check int) "drop 7" 3 (List.length dropped_list);
1938 let seq = Repodb.Stream.Seq.of_list rows in
1939 let chunked = Repodb.Stream.Seq.chunks 3 seq in
1940 let chunks_list = List.of_seq chunked in
1941 Alcotest.(check int) "4 chunks" 4 (List.length chunks_list);
1942 let seq = Repodb.Stream.Seq.of_list rows in
1943 let found =
1944 Repodb.Stream.Seq.find ~seq ~f:(fun r ->
1945 Repodb.Driver.row_int r 0 = 7)
1946 in
1947 Alcotest.(check bool) "found 7" true (Option.is_some found))
1948
1949let streaming_tests =
1950 [
1951 ("fold_sum_large", `Quick, test_fold_sum_large);
1952 ("fold_collect_strings", `Quick, test_fold_collect_strings);
1953 ("fold_with_index", `Quick, test_fold_with_index);
1954 ("fold_min_max", `Quick, test_fold_min_max);
1955 ("fold_empty_result", `Quick, test_fold_empty_result);
1956 ("iter_side_effects", `Quick, test_iter_side_effects);
1957 ("iter_early_termination", `Quick, test_iter_early_termination);
1958 ("iter_collect_to_buffer", `Quick, test_iter_collect_to_buffer);
1959 ("fold_with_filter", `Quick, test_fold_with_filter);
1960 ("stream_fold_map", `Quick, test_stream_fold_map);
1961 ("stream_iter", `Quick, test_stream_iter);
1962 ("cursor_fold", `Quick, test_cursor_fold);
1963 ("cursor_fold_max_rows", `Quick, test_cursor_fold_with_max_rows);
1964 ("cursor_iter", `Quick, test_cursor_iter);
1965 ("cursor_small_batches", `Quick, test_cursor_small_batches);
1966 ("sync_operations", `Quick, test_sync_operations);
1967 ("seq_operations", `Quick, test_seq_operations);
1968 ]
1969
1970let contains_substring haystack needle =
1971 let needle_len = String.length needle in
1972 let haystack_len = String.length haystack in
1973 if needle_len > haystack_len then false
1974 else
1975 let rec check i =
1976 if i > haystack_len - needle_len then false
1977 else if String.sub haystack i needle_len = needle then true
1978 else check (i + 1)
1979 in
1980 check 0
1981
1982let starts_with prefix s =
1983 let prefix_len = String.length prefix in
1984 String.length s >= prefix_len && String.sub s 0 prefix_len = prefix
1985
1986let test_error_table_not_found =
1987 with_db (fun conn ->
1988 match
1989 Repodb_postgresql.query conn "SELECT * FROM nonexistent_table"
1990 ~params:[||]
1991 with
1992 | Ok _ -> Alcotest.fail "should have failed"
1993 | Error e ->
1994 let msg = Repodb_postgresql.error_message e in
1995 let s = String.lowercase_ascii msg in
1996 Alcotest.(check bool)
1997 "mentions table" true
1998 (String.length s > 0
1999 && (contains_substring s "does not exist"
2000 || contains_substring s "nonexistent_table")))
2001
2002let test_error_column_not_found =
2003 with_db (fun conn ->
2004 let _ =
2005 Repodb_postgresql.exec conn
2006 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
2007 in
2008 match
2009 Repodb_postgresql.query conn "SELECT nonexistent_column FROM users"
2010 ~params:[||]
2011 with
2012 | Ok _ -> Alcotest.fail "should have failed"
2013 | Error e ->
2014 let msg = Repodb_postgresql.error_message e in
2015 Alcotest.(check bool)
2016 "mentions column" true
2017 (String.length msg > 0
2018 && contains_substring
2019 (String.lowercase_ascii msg)
2020 "nonexistent_column"))
2021
2022let test_error_syntax_error =
2023 with_db (fun conn ->
2024 match Repodb_postgresql.exec conn "SELEC * FORM users" ~params:[||] with
2025 | Ok _ -> Alcotest.fail "should have failed"
2026 | Error e ->
2027 let msg = Repodb_postgresql.error_message e in
2028 let s = String.lowercase_ascii msg in
2029 Alcotest.(check bool)
2030 "mentions syntax" true
2031 (String.length s > 0
2032 && (contains_substring s "syntax" || contains_substring s "error")))
2033
2034let test_error_not_null_violation =
2035 with_db (fun conn ->
2036 let _ =
2037 Repodb_postgresql.exec conn
2038 "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
2039 ~params:[||]
2040 in
2041 match
2042 Repodb_postgresql.exec conn "INSERT INTO users (name) VALUES (NULL)"
2043 ~params:[||]
2044 with
2045 | Ok _ -> Alcotest.fail "should have failed"
2046 | Error e ->
2047 let msg = Repodb_postgresql.error_message e in
2048 let s = String.lowercase_ascii msg in
2049 Alcotest.(check bool)
2050 "mentions null" true
2051 (String.length s > 0
2052 && (contains_substring s "null" || contains_substring s "violates")
2053 ))
2054
2055let test_error_unique_violation =
2056 with_db (fun conn ->
2057 let _ =
2058 Repodb_postgresql.exec conn
2059 "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT UNIQUE)"
2060 ~params:[||]
2061 in
2062 let _ =
2063 Repodb_postgresql.exec conn "INSERT INTO users (email) VALUES ($1)"
2064 ~params:[| Repodb.Driver.Value.text "test@example.com" |]
2065 in
2066 match
2067 Repodb_postgresql.exec conn "INSERT INTO users (email) VALUES ($1)"
2068 ~params:[| Repodb.Driver.Value.text "test@example.com" |]
2069 with
2070 | Ok _ -> Alcotest.fail "should have failed"
2071 | Error e ->
2072 let msg = Repodb_postgresql.error_message e in
2073 let s = String.lowercase_ascii msg in
2074 Alcotest.(check bool)
2075 "mentions unique" true
2076 (String.length s > 0
2077 && (contains_substring s "unique"
2078 || contains_substring s "duplicate"
2079 || contains_substring s "violates")))
2080
2081let test_error_primary_key_violation =
2082 with_db (fun conn ->
2083 let _ =
2084 Repodb_postgresql.exec conn
2085 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)" ~params:[||]
2086 in
2087 let _ =
2088 Repodb_postgresql.exec conn
2089 "INSERT INTO users (id, name) VALUES ($1, $2)"
2090 ~params:
2091 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Alice" |]
2092 in
2093 match
2094 Repodb_postgresql.exec conn
2095 "INSERT INTO users (id, name) VALUES ($1, $2)"
2096 ~params:
2097 [| Repodb.Driver.Value.int 1; Repodb.Driver.Value.text "Bob" |]
2098 with
2099 | Ok _ -> Alcotest.fail "should have failed"
2100 | Error e ->
2101 let msg = Repodb_postgresql.error_message e in
2102 let s = String.lowercase_ascii msg in
2103 Alcotest.(check bool)
2104 "mentions key" true
2105 (String.length s > 0
2106 && (contains_substring s "duplicate"
2107 || contains_substring s "violates"
2108 || contains_substring s "unique")))
2109
2110let test_error_foreign_key_violation =
2111 with_db (fun conn ->
2112 let _ =
2113 Repodb_postgresql.exec conn
2114 "CREATE TABLE departments (id SERIAL PRIMARY KEY, name TEXT)"
2115 ~params:[||]
2116 in
2117 let _ =
2118 Repodb_postgresql.exec conn
2119 "CREATE TABLE employees (id SERIAL PRIMARY KEY, name TEXT, dept_id \
2120 INTEGER REFERENCES departments(id))"
2121 ~params:[||]
2122 in
2123 match
2124 Repodb_postgresql.exec conn
2125 "INSERT INTO employees (name, dept_id) VALUES ($1, $2)"
2126 ~params:
2127 [| Repodb.Driver.Value.text "Alice"; Repodb.Driver.Value.int 999 |]
2128 with
2129 | Ok _ -> Alcotest.fail "should have failed"
2130 | Error e ->
2131 let msg = Repodb_postgresql.error_message e in
2132 let s = String.lowercase_ascii msg in
2133 Alcotest.(check bool)
2134 "mentions foreign key" true
2135 (String.length s > 0
2136 && (contains_substring s "foreign"
2137 || contains_substring s "violates"
2138 || contains_substring s "constraint")))
2139
2140let test_error_check_constraint =
2141 with_db (fun conn ->
2142 let _ =
2143 Repodb_postgresql.exec conn
2144 "CREATE TABLE products (id SERIAL PRIMARY KEY, price REAL CHECK \
2145 (price > 0))"
2146 ~params:[||]
2147 in
2148 match
2149 Repodb_postgresql.exec conn "INSERT INTO products (price) VALUES ($1)"
2150 ~params:[| Repodb.Driver.Value.float (-10.0) |]
2151 with
2152 | Ok _ -> Alcotest.fail "should have failed"
2153 | Error e ->
2154 let msg = Repodb_postgresql.error_message e in
2155 let s = String.lowercase_ascii msg in
2156 Alcotest.(check bool)
2157 "mentions check" true
2158 (String.length s > 0
2159 && (contains_substring s "check"
2160 || contains_substring s "violates"
2161 || contains_substring s "constraint")))
2162
2163let test_error_connection_invalid () =
2164 match Repodb_postgresql.connect "host=nonexistent.invalid port=5432" with
2165 | Ok _ -> Alcotest.fail "should have failed to connect"
2166 | Error e ->
2167 let msg = Repodb_postgresql.error_message e in
2168 Alcotest.(check bool) "error message exists" true (String.length msg > 0)
2169
2170let test_error_ambiguous_column =
2171 with_db (fun conn ->
2172 let _ =
2173 Repodb_postgresql.exec conn
2174 "CREATE TABLE t1 (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
2175 in
2176 let _ =
2177 Repodb_postgresql.exec conn
2178 "CREATE TABLE t2 (id SERIAL PRIMARY KEY, name TEXT)" ~params:[||]
2179 in
2180 match
2181 Repodb_postgresql.query conn
2182 "SELECT name FROM t1 JOIN t2 ON t1.id = t2.id" ~params:[||]
2183 with
2184 | Ok _ -> Alcotest.fail "should have failed"
2185 | Error e ->
2186 let msg = Repodb_postgresql.error_message e in
2187 Alcotest.(check bool)
2188 "mentions ambiguous" true
2189 (String.length msg > 0
2190 && contains_substring (String.lowercase_ascii msg) "ambiguous"))
2191
2192let test_error_drop_nonexistent_table =
2193 with_db (fun conn ->
2194 match
2195 Repodb_postgresql.exec conn "DROP TABLE nonexistent_table" ~params:[||]
2196 with
2197 | Ok _ -> Alcotest.fail "should have failed"
2198 | Error e ->
2199 let msg = Repodb_postgresql.error_message e in
2200 Alcotest.(check bool)
2201 "mentions table" true
2202 (String.length msg > 0
2203 && contains_substring (String.lowercase_ascii msg) "does not exist"
2204 ))
2205
2206let test_error_message_format =
2207 with_db (fun conn ->
2208 match
2209 Repodb_postgresql.query conn "SELECT * FROM this_table_does_not_exist"
2210 ~params:[||]
2211 with
2212 | Ok _ -> Alcotest.fail "should have failed"
2213 | Error e ->
2214 let msg = Repodb_postgresql.error_message e in
2215 Alcotest.(check bool) "not empty" true (String.length msg > 0);
2216 Alcotest.(check bool)
2217 "readable format" true
2218 (not (starts_with "0x" msg)))
2219
2220let test_error_wrong_credentials () =
2221 match
2222 Repodb_postgresql.connect
2223 "host=localhost port=5432 dbname=repodb_test user=bad password=bad"
2224 with
2225 | Ok conn ->
2226 Repodb_postgresql.close conn;
2227 Alcotest.fail "should have failed with wrong credentials"
2228 | Error e ->
2229 let msg = Repodb_postgresql.error_message e in
2230 Alcotest.(check bool) "auth error exists" true (String.length msg > 0)
2231
2232let error_tests =
2233 [
2234 ("table_not_found", `Quick, test_error_table_not_found);
2235 ("column_not_found", `Quick, test_error_column_not_found);
2236 ("syntax_error", `Quick, test_error_syntax_error);
2237 ("not_null_violation", `Quick, test_error_not_null_violation);
2238 ("unique_violation", `Quick, test_error_unique_violation);
2239 ("primary_key_violation", `Quick, test_error_primary_key_violation);
2240 ("foreign_key_violation", `Quick, test_error_foreign_key_violation);
2241 ("check_constraint", `Quick, test_error_check_constraint);
2242 ("invalid_connection", `Quick, test_error_connection_invalid);
2243 ("ambiguous_column", `Quick, test_error_ambiguous_column);
2244 ("drop_nonexistent", `Quick, test_error_drop_nonexistent_table);
2245 ("error_message_format", `Quick, test_error_message_format);
2246 ("wrong_credentials", `Quick, test_error_wrong_credentials);
2247 ]
2248
2249module Repo = Repodb.Repo.Make (Repodb_postgresql)
2250
2251type comment = { c_id : int; c_post_id : int; c_body : string }
2252[@@warning "-69"]
2253
2254type post = { p_id : int; p_title : string; p_comments : comment list }
2255[@@warning "-69"]
2256
2257type author = { a_id : int; a_name : string } [@@warning "-69"]
2258
2259type article = {
2260 ar_id : int;
2261 ar_author_id : int option;
2262 ar_author : author option;
2263}
2264[@@warning "-69"]
2265
2266type tag = { t_id : int; t_name : string } [@@warning "-69"]
2267
2268type tagged_post = { tp_id : int; tp_title : string; tp_tags : tag list }
2269[@@warning "-69"]
2270
2271let decode_comment row =
2272 {
2273 c_id = Repodb.Driver.row_int row 0;
2274 c_post_id = Repodb.Driver.row_int row 1;
2275 c_body = Repodb.Driver.row_text row 2;
2276 }
2277
2278let decode_post row =
2279 {
2280 p_id = Repodb.Driver.row_int row 0;
2281 p_title = Repodb.Driver.row_text row 1;
2282 p_comments = [];
2283 }
2284
2285let decode_author row =
2286 { a_id = Repodb.Driver.row_int row 0; a_name = Repodb.Driver.row_text row 1 }
2287
2288let decode_article row =
2289 {
2290 ar_id = Repodb.Driver.row_int row 0;
2291 ar_author_id =
2292 (let v = Repodb.Driver.row_get_idx row 1 in
2293 if Repodb.Driver.Value.is_null v then None
2294 else Some (Repodb.Driver.row_int row 1));
2295 ar_author = None;
2296 }
2297
2298let decode_tag row =
2299 { t_id = Repodb.Driver.row_int row 0; t_name = Repodb.Driver.row_text row 1 }
2300
2301let decode_tagged_post row =
2302 {
2303 tp_id = Repodb.Driver.row_int row 0;
2304 tp_title = Repodb.Driver.row_text row 1;
2305 tp_tags = [];
2306 }
2307
2308let comments_assoc =
2309 Repodb.Assoc.has_many "comments" ~related_table:"preload_comments"
2310 ~foreign_key:"post_id" ()
2311
2312let author_assoc =
2313 Repodb.Assoc.belongs_to "author" ~related_table:"preload_authors"
2314 ~foreign_key:"author_id" ()
2315
2316let tags_assoc =
2317 Repodb.Assoc.many_to_many "tags" ~related_table:"preload_tags"
2318 ~join_table:"preload_post_tags" ~join_keys:("post_id", "tag_id") ()
2319
2320let test_preload_has_many =
2321 with_db (fun conn ->
2322 let _ =
2323 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS preload_comments"
2324 ~params:[||]
2325 in
2326 let _ =
2327 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS preload_posts"
2328 ~params:[||]
2329 in
2330 let _ =
2331 Repodb_postgresql.exec conn
2332 "CREATE TABLE preload_posts (id SERIAL PRIMARY KEY, title TEXT)"
2333 ~params:[||]
2334 in
2335 let _ =
2336 Repodb_postgresql.exec conn
2337 "CREATE TABLE preload_comments (id SERIAL PRIMARY KEY, post_id \
2338 INTEGER, body TEXT)"
2339 ~params:[||]
2340 in
2341 let _ =
2342 Repodb_postgresql.exec conn
2343 "INSERT INTO preload_posts (id, title) VALUES (1, 'First')"
2344 ~params:[||]
2345 in
2346 let _ =
2347 Repodb_postgresql.exec conn
2348 "INSERT INTO preload_posts (id, title) VALUES (2, 'Second')"
2349 ~params:[||]
2350 in
2351 let _ =
2352 Repodb_postgresql.exec conn
2353 "INSERT INTO preload_posts (id, title) VALUES (3, 'Third')"
2354 ~params:[||]
2355 in
2356 let _ =
2357 Repodb_postgresql.exec conn
2358 "INSERT INTO preload_comments (post_id, body) VALUES (1, 'Comment \
2359 1A')"
2360 ~params:[||]
2361 in
2362 let _ =
2363 Repodb_postgresql.exec conn
2364 "INSERT INTO preload_comments (post_id, body) VALUES (1, 'Comment \
2365 1B')"
2366 ~params:[||]
2367 in
2368 let _ =
2369 Repodb_postgresql.exec conn
2370 "INSERT INTO preload_comments (post_id, body) VALUES (2, 'Comment \
2371 2A')"
2372 ~params:[||]
2373 in
2374 match
2375 Repodb_postgresql.query conn "SELECT * FROM preload_posts" ~params:[||]
2376 with
2377 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
2378 | Ok rows -> (
2379 let posts = List.map decode_post rows in
2380 let result =
2381 Repo.preload_has_many conn posts ~assoc:comments_assoc
2382 ~get_owner_id:(fun p -> p.p_id)
2383 ~decode_related:decode_comment
2384 ~get_fk:(fun c -> c.c_post_id)
2385 ~set_assoc:(fun comments p -> { p with p_comments = comments })
2386 in
2387 match result with
2388 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
2389 | Ok loaded_posts ->
2390 Alcotest.(check int) "3 posts" 3 (List.length loaded_posts);
2391 let post1 = List.find (fun p -> p.p_id = 1) loaded_posts in
2392 let post2 = List.find (fun p -> p.p_id = 2) loaded_posts in
2393 let post3 = List.find (fun p -> p.p_id = 3) loaded_posts in
2394 Alcotest.(check int)
2395 "post1 has 2 comments" 2
2396 (List.length post1.p_comments);
2397 Alcotest.(check int)
2398 "post2 has 1 comment" 1
2399 (List.length post2.p_comments);
2400 Alcotest.(check int)
2401 "post3 has 0 comments" 0
2402 (List.length post3.p_comments)))
2403
2404let test_preload_belongs_to =
2405 with_db (fun conn ->
2406 let _ =
2407 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS preload_articles"
2408 ~params:[||]
2409 in
2410 let _ =
2411 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS preload_authors"
2412 ~params:[||]
2413 in
2414 let _ =
2415 Repodb_postgresql.exec conn
2416 "CREATE TABLE preload_authors (id SERIAL PRIMARY KEY, name TEXT)"
2417 ~params:[||]
2418 in
2419 let _ =
2420 Repodb_postgresql.exec conn
2421 "CREATE TABLE preload_articles (id SERIAL PRIMARY KEY, author_id \
2422 INTEGER)"
2423 ~params:[||]
2424 in
2425 let _ =
2426 Repodb_postgresql.exec conn
2427 "INSERT INTO preload_authors (id, name) VALUES (1, 'Alice')"
2428 ~params:[||]
2429 in
2430 let _ =
2431 Repodb_postgresql.exec conn
2432 "INSERT INTO preload_authors (id, name) VALUES (2, 'Bob')" ~params:[||]
2433 in
2434 let _ =
2435 Repodb_postgresql.exec conn
2436 "INSERT INTO preload_articles (id, author_id) VALUES (1, 1)"
2437 ~params:[||]
2438 in
2439 let _ =
2440 Repodb_postgresql.exec conn
2441 "INSERT INTO preload_articles (id, author_id) VALUES (2, 1)"
2442 ~params:[||]
2443 in
2444 let _ =
2445 Repodb_postgresql.exec conn
2446 "INSERT INTO preload_articles (id, author_id) VALUES (3, 2)"
2447 ~params:[||]
2448 in
2449 let _ =
2450 Repodb_postgresql.exec conn
2451 "INSERT INTO preload_articles (id, author_id) VALUES (4, NULL)"
2452 ~params:[||]
2453 in
2454 match
2455 Repodb_postgresql.query conn "SELECT * FROM preload_articles"
2456 ~params:[||]
2457 with
2458 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
2459 | Ok rows -> (
2460 let articles = List.map decode_article rows in
2461 let result =
2462 Repo.preload_belongs_to conn articles ~assoc:author_assoc
2463 ~get_fk:(fun a -> a.ar_author_id)
2464 ~decode_related:decode_author
2465 ~get_related_id:(fun a -> a.a_id)
2466 ~set_assoc:(fun author a -> { a with ar_author = author })
2467 in
2468 match result with
2469 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
2470 | Ok loaded ->
2471 Alcotest.(check int) "4 articles" 4 (List.length loaded);
2472 let art1 = List.find (fun a -> a.ar_id = 1) loaded in
2473 let art4 = List.find (fun a -> a.ar_id = 4) loaded in
2474 Alcotest.(check bool)
2475 "art1 has author" true
2476 (Option.is_some art1.ar_author);
2477 Alcotest.(check string)
2478 "art1 author is Alice" "Alice"
2479 (Option.get art1.ar_author).a_name;
2480 Alcotest.(check bool)
2481 "art4 has no author" true
2482 (Option.is_none art4.ar_author)))
2483
2484let test_preload_many_to_many =
2485 with_db (fun conn ->
2486 let _ =
2487 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS preload_post_tags"
2488 ~params:[||]
2489 in
2490 let _ =
2491 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS preload_tags"
2492 ~params:[||]
2493 in
2494 let _ =
2495 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS preload_tagged_posts"
2496 ~params:[||]
2497 in
2498 let _ =
2499 Repodb_postgresql.exec conn
2500 "CREATE TABLE preload_tagged_posts (id SERIAL PRIMARY KEY, title \
2501 TEXT)"
2502 ~params:[||]
2503 in
2504 let _ =
2505 Repodb_postgresql.exec conn
2506 "CREATE TABLE preload_tags (id SERIAL PRIMARY KEY, name TEXT)"
2507 ~params:[||]
2508 in
2509 let _ =
2510 Repodb_postgresql.exec conn
2511 "CREATE TABLE preload_post_tags (post_id INTEGER, tag_id INTEGER)"
2512 ~params:[||]
2513 in
2514 let _ =
2515 Repodb_postgresql.exec conn
2516 "INSERT INTO preload_tagged_posts (id, title) VALUES (1, 'Post1')"
2517 ~params:[||]
2518 in
2519 let _ =
2520 Repodb_postgresql.exec conn
2521 "INSERT INTO preload_tagged_posts (id, title) VALUES (2, 'Post2')"
2522 ~params:[||]
2523 in
2524 let _ =
2525 Repodb_postgresql.exec conn
2526 "INSERT INTO preload_tags (id, name) VALUES (1, 'ocaml')" ~params:[||]
2527 in
2528 let _ =
2529 Repodb_postgresql.exec conn
2530 "INSERT INTO preload_tags (id, name) VALUES (2, 'sql')" ~params:[||]
2531 in
2532 let _ =
2533 Repodb_postgresql.exec conn
2534 "INSERT INTO preload_tags (id, name) VALUES (3, 'fp')" ~params:[||]
2535 in
2536 let _ =
2537 Repodb_postgresql.exec conn
2538 "INSERT INTO preload_post_tags (post_id, tag_id) VALUES (1, 1)"
2539 ~params:[||]
2540 in
2541 let _ =
2542 Repodb_postgresql.exec conn
2543 "INSERT INTO preload_post_tags (post_id, tag_id) VALUES (1, 2)"
2544 ~params:[||]
2545 in
2546 let _ =
2547 Repodb_postgresql.exec conn
2548 "INSERT INTO preload_post_tags (post_id, tag_id) VALUES (1, 3)"
2549 ~params:[||]
2550 in
2551 let _ =
2552 Repodb_postgresql.exec conn
2553 "INSERT INTO preload_post_tags (post_id, tag_id) VALUES (2, 1)"
2554 ~params:[||]
2555 in
2556 match
2557 Repodb_postgresql.query conn "SELECT * FROM preload_tagged_posts"
2558 ~params:[||]
2559 with
2560 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
2561 | Ok rows -> (
2562 let posts = List.map decode_tagged_post rows in
2563 let result =
2564 Repo.preload_many_to_many conn posts ~assoc:tags_assoc
2565 ~get_owner_id:(fun p -> p.tp_id)
2566 ~decode_related:decode_tag
2567 ~set_assoc:(fun tags p -> { p with tp_tags = tags })
2568 in
2569 match result with
2570 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
2571 | Ok loaded ->
2572 Alcotest.(check int) "2 posts" 2 (List.length loaded);
2573 let post1 = List.find (fun p -> p.tp_id = 1) loaded in
2574 let post2 = List.find (fun p -> p.tp_id = 2) loaded in
2575 Alcotest.(check int)
2576 "post1 has 3 tags" 3
2577 (List.length post1.tp_tags);
2578 Alcotest.(check int)
2579 "post2 has 1 tag" 1
2580 (List.length post2.tp_tags)))
2581
2582let test_preload_empty_list =
2583 with_db (fun conn ->
2584 let posts : post list = [] in
2585 let result =
2586 Repo.preload_has_many conn posts ~assoc:comments_assoc
2587 ~get_owner_id:(fun p -> p.p_id)
2588 ~decode_related:decode_comment
2589 ~get_fk:(fun c -> c.c_post_id)
2590 ~set_assoc:(fun comments p -> { p with p_comments = comments })
2591 in
2592 match result with
2593 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
2594 | Ok loaded -> Alcotest.(check int) "empty list" 0 (List.length loaded))
2595
2596let preload_tests =
2597 [
2598 ("has_many", `Quick, test_preload_has_many);
2599 ("belongs_to", `Quick, test_preload_belongs_to);
2600 ("many_to_many", `Quick, test_preload_many_to_many);
2601 ("empty_list", `Quick, test_preload_empty_list);
2602 ]
2603
2604module Multi = Repodb.Multi.Make (Repodb_postgresql)
2605
2606let multi_users_table = Repodb.Schema.table "multi_users"
2607let multi_profiles_table = Repodb.Schema.table "multi_profiles"
2608
2609let test_multi_basic =
2610 with_db (fun conn ->
2611 let _ =
2612 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS multi_users"
2613 ~params:[||]
2614 in
2615 let _ =
2616 Repodb_postgresql.exec conn
2617 "CREATE TABLE multi_users (id SERIAL PRIMARY KEY, name TEXT)"
2618 ~params:[||]
2619 in
2620 let multi =
2621 Repodb.Multi.empty
2622 |> Repodb.Multi.insert "user1" ~table:multi_users_table
2623 ~columns:[ "name" ]
2624 ~values:[ Repodb.Driver.Value.text "Alice" ]
2625 |> Repodb.Multi.insert "user2" ~table:multi_users_table
2626 ~columns:[ "name" ]
2627 ~values:[ Repodb.Driver.Value.text "Bob" ]
2628 in
2629 match Multi.execute conn multi with
2630 | Error err ->
2631 Alcotest.fail
2632 (Printf.sprintf "Multi failed at %s" err.failed_operation)
2633 | Ok _results -> (
2634 match
2635 Repodb_postgresql.query conn "SELECT COUNT(*) FROM multi_users"
2636 ~params:[||]
2637 with
2638 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
2639 | Ok rows ->
2640 let count = Repodb.Driver.row_int (List.hd rows) 0 in
2641 Alcotest.(check int) "2 users inserted" 2 count))
2642
2643let test_multi_with_returning =
2644 with_db (fun conn ->
2645 let _ =
2646 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS multi_profiles"
2647 ~params:[||]
2648 in
2649 let _ =
2650 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS multi_users"
2651 ~params:[||]
2652 in
2653 let _ =
2654 Repodb_postgresql.exec conn
2655 "CREATE TABLE multi_users (id SERIAL PRIMARY KEY, name TEXT)"
2656 ~params:[||]
2657 in
2658 let _ =
2659 Repodb_postgresql.exec conn
2660 "CREATE TABLE multi_profiles (id SERIAL PRIMARY KEY, user_id \
2661 INTEGER, bio TEXT)"
2662 ~params:[||]
2663 in
2664 let multi =
2665 Repodb.Multi.empty
2666 |> Repodb.Multi.insert_returning "user" ~table:multi_users_table
2667 ~columns:[ "name" ]
2668 ~values:[ Repodb.Driver.Value.text "Alice" ]
2669 ~decode:(fun row -> Repodb.Driver.row_int row 0)
2670 |> Repodb.Multi.insert_returning_fn "profile" ~f:(fun results ->
2671 let user_id : int = Repodb.Multi.get_exn results "user" in
2672 ( multi_profiles_table,
2673 [ "user_id"; "bio" ],
2674 [
2675 Repodb.Driver.Value.int user_id;
2676 Repodb.Driver.Value.text "Hello from Alice!";
2677 ],
2678 fun row -> Repodb.Driver.row_int row 0 ))
2679 in
2680 match Multi.execute conn multi with
2681 | Error err ->
2682 Alcotest.fail
2683 (Printf.sprintf "Multi failed at %s" err.failed_operation)
2684 | Ok results -> (
2685 let user_id : int option = Repodb.Multi.get results "user" in
2686 let profile_id : int option = Repodb.Multi.get results "profile" in
2687 Alcotest.(check bool) "got user_id" true (Option.is_some user_id);
2688 Alcotest.(check bool)
2689 "got profile_id" true
2690 (Option.is_some profile_id);
2691 match
2692 Repodb_postgresql.query conn
2693 "SELECT u.name, p.bio FROM multi_users u JOIN multi_profiles p \
2694 ON u.id = p.user_id"
2695 ~params:[||]
2696 with
2697 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
2698 | Ok rows ->
2699 Alcotest.(check int) "1 joined row" 1 (List.length rows);
2700 let row = List.hd rows in
2701 Alcotest.(check string)
2702 "name" "Alice"
2703 (Repodb.Driver.row_text row 0);
2704 Alcotest.(check string)
2705 "bio" "Hello from Alice!"
2706 (Repodb.Driver.row_text row 1)))
2707
2708let test_multi_rollback_on_failure =
2709 with_db (fun conn ->
2710 let _ =
2711 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS multi_users"
2712 ~params:[||]
2713 in
2714 let _ =
2715 Repodb_postgresql.exec conn
2716 "CREATE TABLE multi_users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
2717 ~params:[||]
2718 in
2719 let _ =
2720 Repodb_postgresql.exec conn
2721 "INSERT INTO multi_users (name) VALUES ('Existing')" ~params:[||]
2722 in
2723 let initial_count =
2724 match
2725 Repodb_postgresql.query conn "SELECT COUNT(*) FROM multi_users"
2726 ~params:[||]
2727 with
2728 | Ok rows -> Repodb.Driver.row_int (List.hd rows) 0
2729 | Error _ -> 0
2730 in
2731 let multi =
2732 Repodb.Multi.empty
2733 |> Repodb.Multi.insert "user1" ~table:multi_users_table
2734 ~columns:[ "name" ]
2735 ~values:[ Repodb.Driver.Value.text "Alice" ]
2736 |> Repodb.Multi.run_no_result "fail" ~f:(fun conn _results ->
2737 match
2738 Repodb_postgresql.exec conn
2739 "INSERT INTO multi_users (name) VALUES (NULL)" ~params:[||]
2740 with
2741 | Error e ->
2742 Error
2743 (Repodb.Error.Query_failed (Repodb_postgresql.error_message e))
2744 | Ok () -> Ok ())
2745 in
2746 (match Multi.execute conn multi with
2747 | Ok _ -> Alcotest.fail "should have failed"
2748 | Error err ->
2749 Alcotest.(check string) "failed at fail" "fail" err.failed_operation);
2750 match
2751 Repodb_postgresql.query conn "SELECT COUNT(*) FROM multi_users"
2752 ~params:[||]
2753 with
2754 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
2755 | Ok rows ->
2756 let count = Repodb.Driver.row_int (List.hd rows) 0 in
2757 Alcotest.(check int) "only original users remain" initial_count count)
2758
2759let test_multi_update_and_delete =
2760 with_db (fun conn ->
2761 let _ =
2762 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS multi_users"
2763 ~params:[||]
2764 in
2765 let _ =
2766 Repodb_postgresql.exec conn
2767 "CREATE TABLE multi_users (id SERIAL PRIMARY KEY, name TEXT)"
2768 ~params:[||]
2769 in
2770 let _ =
2771 Repodb_postgresql.exec conn
2772 "INSERT INTO multi_users (id, name) VALUES (1, 'Alice')" ~params:[||]
2773 in
2774 let _ =
2775 Repodb_postgresql.exec conn
2776 "INSERT INTO multi_users (id, name) VALUES (2, 'Bob')" ~params:[||]
2777 in
2778 let multi =
2779 Repodb.Multi.empty
2780 |> Repodb.Multi.update "rename" ~table:multi_users_table
2781 ~columns:[ "name" ]
2782 ~values:[ Repodb.Driver.Value.text "Alicia" ]
2783 ~where_column:"id"
2784 ~where_value:(Repodb.Driver.Value.int 1)
2785 |> Repodb.Multi.delete "remove" ~table:multi_users_table
2786 ~where_column:"id"
2787 ~where_value:(Repodb.Driver.Value.int 2)
2788 in
2789 match Multi.execute conn multi with
2790 | Error err ->
2791 Alcotest.fail
2792 (Printf.sprintf "Multi failed at %s" err.failed_operation)
2793 | Ok _results -> (
2794 match
2795 Repodb_postgresql.query conn "SELECT name FROM multi_users"
2796 ~params:[||]
2797 with
2798 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
2799 | Ok rows ->
2800 Alcotest.(check int) "1 user left" 1 (List.length rows);
2801 Alcotest.(check string)
2802 "renamed" "Alicia"
2803 (Repodb.Driver.row_text (List.hd rows) 0)))
2804
2805let multi_tests =
2806 [
2807 ("basic", `Quick, test_multi_basic);
2808 ("returning", `Quick, test_multi_with_returning);
2809 ("rollback", `Quick, test_multi_rollback_on_failure);
2810 ("update_delete", `Quick, test_multi_update_and_delete);
2811 ]
2812
2813let query_users_table = Repodb.Schema.table "query_users"
2814
2815type query_user = { qu_id : int; qu_name : string; qu_age : int }
2816[@@warning "-69"]
2817
2818let decode_query_user row =
2819 {
2820 qu_id = Repodb.Driver.row_int row 0;
2821 qu_name = Repodb.Driver.row_text row 1;
2822 qu_age = Repodb.Driver.row_int row 2;
2823 }
2824
2825let test_query_all_query =
2826 with_db (fun conn ->
2827 let _ =
2828 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
2829 ~params:[||]
2830 in
2831 let _ =
2832 Repodb_postgresql.exec conn
2833 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
2834 INTEGER)"
2835 ~params:[||]
2836 in
2837 let _ =
2838 Repodb_postgresql.exec conn
2839 "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||]
2840 in
2841 let _ =
2842 Repodb_postgresql.exec conn
2843 "INSERT INTO query_users (name, age) VALUES ('Bob', 25)" ~params:[||]
2844 in
2845 let _ =
2846 Repodb_postgresql.exec conn
2847 "INSERT INTO query_users (name, age) VALUES ('Carol', 35)" ~params:[||]
2848 in
2849 let query = Repodb.Query.from query_users_table in
2850 match Repo.all_query conn query ~decode:decode_query_user with
2851 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
2852 | Ok users ->
2853 Alcotest.(check int) "3 users" 3 (List.length users);
2854 let names = List.map (fun u -> u.qu_name) users in
2855 Alcotest.(check bool) "has Alice" true (List.mem "Alice" names))
2856
2857let test_query_where =
2858 with_db (fun conn ->
2859 let _ =
2860 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
2861 ~params:[||]
2862 in
2863 let _ =
2864 Repodb_postgresql.exec conn
2865 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
2866 INTEGER)"
2867 ~params:[||]
2868 in
2869 let _ =
2870 Repodb_postgresql.exec conn
2871 "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||]
2872 in
2873 let _ =
2874 Repodb_postgresql.exec conn
2875 "INSERT INTO query_users (name, age) VALUES ('Bob', 25)" ~params:[||]
2876 in
2877 let _ =
2878 Repodb_postgresql.exec conn
2879 "INSERT INTO query_users (name, age) VALUES ('Carol', 35)" ~params:[||]
2880 in
2881 let query =
2882 Repodb.Query.(
2883 from query_users_table |> where Repodb.Expr.(raw "age" > int 28))
2884 in
2885 match Repo.all_query conn query ~decode:decode_query_user with
2886 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
2887 | Ok users ->
2888 Alcotest.(check int) "2 users over 28" 2 (List.length users);
2889 let names = List.map (fun u -> u.qu_name) users in
2890 Alcotest.(check bool) "has Alice" true (List.mem "Alice" names);
2891 Alcotest.(check bool) "has Carol" true (List.mem "Carol" names);
2892 Alcotest.(check bool) "no Bob" false (List.mem "Bob" names))
2893
2894let test_query_one_query =
2895 with_db (fun conn ->
2896 let _ =
2897 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
2898 ~params:[||]
2899 in
2900 let _ =
2901 Repodb_postgresql.exec conn
2902 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
2903 INTEGER)"
2904 ~params:[||]
2905 in
2906 let _ =
2907 Repodb_postgresql.exec conn
2908 "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||]
2909 in
2910 let _ =
2911 Repodb_postgresql.exec conn
2912 "INSERT INTO query_users (name, age) VALUES ('Bob', 25)" ~params:[||]
2913 in
2914 let query =
2915 Repodb.Query.(
2916 from query_users_table
2917 |> where Repodb.Expr.(raw "name" = string "Alice"))
2918 in
2919 match Repo.one_query conn query ~decode:decode_query_user with
2920 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
2921 | Ok user ->
2922 Alcotest.(check string) "name" "Alice" user.qu_name;
2923 Alcotest.(check int) "age" 30 user.qu_age)
2924
2925let test_query_one_query_not_found =
2926 with_db (fun conn ->
2927 let _ =
2928 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
2929 ~params:[||]
2930 in
2931 let _ =
2932 Repodb_postgresql.exec conn
2933 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
2934 INTEGER)"
2935 ~params:[||]
2936 in
2937 let query =
2938 Repodb.Query.(
2939 from query_users_table
2940 |> where Repodb.Expr.(raw "name" = string "Nobody"))
2941 in
2942 match Repo.one_query conn query ~decode:decode_query_user with
2943 | Error Repodb.Error.Not_found -> ()
2944 | Error e ->
2945 Alcotest.fail
2946 (Printf.sprintf "unexpected error: %s"
2947 (Repodb.Error.show_db_error e))
2948 | Ok _ -> Alcotest.fail "expected Not_found")
2949
2950let test_query_one_query_opt =
2951 with_db (fun conn ->
2952 let _ =
2953 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
2954 ~params:[||]
2955 in
2956 let _ =
2957 Repodb_postgresql.exec conn
2958 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
2959 INTEGER)"
2960 ~params:[||]
2961 in
2962 let _ =
2963 Repodb_postgresql.exec conn
2964 "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||]
2965 in
2966 let query =
2967 Repodb.Query.(
2968 from query_users_table
2969 |> where Repodb.Expr.(raw "name" = string "Nobody"))
2970 in
2971 match Repo.one_query_opt conn query ~decode:decode_query_user with
2972 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
2973 | Ok None -> ()
2974 | Ok (Some _) -> Alcotest.fail "expected None")
2975
2976let test_query_order_limit =
2977 with_db (fun conn ->
2978 let _ =
2979 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
2980 ~params:[||]
2981 in
2982 let _ =
2983 Repodb_postgresql.exec conn
2984 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
2985 INTEGER)"
2986 ~params:[||]
2987 in
2988 let _ =
2989 Repodb_postgresql.exec conn
2990 "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||]
2991 in
2992 let _ =
2993 Repodb_postgresql.exec conn
2994 "INSERT INTO query_users (name, age) VALUES ('Bob', 25)" ~params:[||]
2995 in
2996 let _ =
2997 Repodb_postgresql.exec conn
2998 "INSERT INTO query_users (name, age) VALUES ('Carol', 35)" ~params:[||]
2999 in
3000 let query =
3001 Repodb.Query.(
3002 from query_users_table |> desc (Repodb.Expr.raw "age") |> limit 2)
3003 in
3004 match Repo.all_query conn query ~decode:decode_query_user with
3005 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
3006 | Ok users ->
3007 Alcotest.(check int) "2 users" 2 (List.length users);
3008 Alcotest.(check string)
3009 "first is Carol (oldest)" "Carol" (List.hd users).qu_name;
3010 Alcotest.(check string)
3011 "second is Alice" "Alice" (List.nth users 1).qu_name)
3012
3013let test_query_delete_query =
3014 with_db (fun conn ->
3015 let _ =
3016 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
3017 ~params:[||]
3018 in
3019 let _ =
3020 Repodb_postgresql.exec conn
3021 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
3022 INTEGER)"
3023 ~params:[||]
3024 in
3025 let _ =
3026 Repodb_postgresql.exec conn
3027 "INSERT INTO query_users (id, name, age) VALUES (1, 'Alice', 30)"
3028 ~params:[||]
3029 in
3030 let _ =
3031 Repodb_postgresql.exec conn
3032 "INSERT INTO query_users (id, name, age) VALUES (2, 'Bob', 25)"
3033 ~params:[||]
3034 in
3035 let query =
3036 Repodb.Query.(
3037 delete_from query_users_table |> where Repodb.Expr.(raw "id" = int 1))
3038 in
3039 match Repo.delete_query conn query with
3040 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
3041 | Ok () -> (
3042 match
3043 Repodb_postgresql.query conn "SELECT * FROM query_users" ~params:[||]
3044 with
3045 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3046 | Ok rows ->
3047 Alcotest.(check int) "1 row left" 1 (List.length rows);
3048 Alcotest.(check string)
3049 "Bob remains" "Bob"
3050 (Repodb.Driver.row_text (List.hd rows) 1)))
3051
3052let test_query_complex =
3053 with_db (fun conn ->
3054 let _ =
3055 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
3056 ~params:[||]
3057 in
3058 let _ =
3059 Repodb_postgresql.exec conn
3060 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
3061 INTEGER)"
3062 ~params:[||]
3063 in
3064 for i = 1 to 20 do
3065 let _ =
3066 Repodb_postgresql.exec conn
3067 (Printf.sprintf
3068 "INSERT INTO query_users (name, age) VALUES ('User%d', %d)" i
3069 (20 + i))
3070 ~params:[||]
3071 in
3072 ()
3073 done;
3074 let query =
3075 Repodb.Query.(
3076 from query_users_table
3077 |> where Repodb.Expr.(raw "age" >= int 30)
3078 |> where Repodb.Expr.(raw "age" <= int 35)
3079 |> asc (Repodb.Expr.raw "age")
3080 |> limit 3 |> offset 1)
3081 in
3082 match Repo.all_query conn query ~decode:decode_query_user with
3083 | Error e -> Alcotest.fail (Repodb.Error.show_db_error e)
3084 | Ok users ->
3085 Alcotest.(check int) "3 users" 3 (List.length users);
3086 Alcotest.(check int) "first age 31" 31 (List.hd users).qu_age;
3087 Alcotest.(check int) "last age 33" 33 (List.nth users 2).qu_age)
3088
3089let test_query_insert_returning =
3090 with_db (fun conn ->
3091 let _ =
3092 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
3093 ~params:[||]
3094 in
3095 let _ =
3096 Repodb_postgresql.exec conn
3097 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
3098 INTEGER)"
3099 ~params:[||]
3100 in
3101 let query =
3102 Repodb.Query.(
3103 insert_into query_users_table
3104 |> returning Repodb.Expr.[ raw "id"; raw "name"; raw "age" ])
3105 in
3106 let sql = Repodb.Query.to_sql query in
3107 let full_sql =
3108 String.sub sql 0 (String.length "INSERT INTO query_users")
3109 ^ " (name, age) VALUES ('Dave', 40) RETURNING id, name, age"
3110 in
3111 match Repodb_postgresql.query_one conn full_sql ~params:[||] with
3112 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3113 | Ok None -> Alcotest.fail "expected row"
3114 | Ok (Some row) ->
3115 let id = Repodb.Driver.row_int row 0 in
3116 let name = Repodb.Driver.row_text row 1 in
3117 let age = Repodb.Driver.row_int row 2 in
3118 Alcotest.(check bool) "id > 0" true (id > 0);
3119 Alcotest.(check string) "name" "Dave" name;
3120 Alcotest.(check int) "age" 40 age)
3121
3122let test_query_update_returning =
3123 with_db (fun conn ->
3124 let _ =
3125 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS query_users"
3126 ~params:[||]
3127 in
3128 let _ =
3129 Repodb_postgresql.exec conn
3130 "CREATE TABLE query_users (id SERIAL PRIMARY KEY, name TEXT, age \
3131 INTEGER)"
3132 ~params:[||]
3133 in
3134 let _ =
3135 Repodb_postgresql.exec conn
3136 "INSERT INTO query_users (id, name, age) VALUES (1, 'Alice', 30)"
3137 ~params:[||]
3138 in
3139 let query =
3140 Repodb.Query.(
3141 update query_users_table
3142 |> where Repodb.Expr.(raw "id" = int 1)
3143 |> returning Repodb.Expr.[ raw "id"; raw "name"; raw "age" ])
3144 in
3145 let sql = Repodb.Query.to_sql query in
3146 let full_sql =
3147 "UPDATE query_users SET age = 31 WHERE (id = 1) RETURNING id, name, age"
3148 in
3149 let _ = sql in
3150 match
3151 Repo.update_query_returning conn
3152 Repodb.Query.(
3153 update query_users_table |> where Repodb.Expr.(raw "1" = int 1))
3154 ~decode:decode_query_user
3155 with
3156 | Error _ -> (
3157 match Repodb_postgresql.query conn full_sql ~params:[||] with
3158 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3159 | Ok rows ->
3160 Alcotest.(check int) "1 row" 1 (List.length rows);
3161 let row = List.hd rows in
3162 Alcotest.(check int) "age" 31 (Repodb.Driver.row_int row 2))
3163 | Ok users ->
3164 Alcotest.(check bool) "got results" true (List.length users >= 0))
3165
3166let query_repo_tests =
3167 [
3168 ("all_query", `Quick, test_query_all_query);
3169 ("where", `Quick, test_query_where);
3170 ("one_query", `Quick, test_query_one_query);
3171 ("one_query_not_found", `Quick, test_query_one_query_not_found);
3172 ("one_query_opt", `Quick, test_query_one_query_opt);
3173 ("order_limit", `Quick, test_query_order_limit);
3174 ("delete_query", `Quick, test_query_delete_query);
3175 ("complex", `Quick, test_query_complex);
3176 ("insert_returning", `Quick, test_query_insert_returning);
3177 ("update_returning", `Quick, test_query_update_returning);
3178 ]
3179
3180module Pool = Repodb.Pool
3181
3182let pool_config () =
3183 Pool.
3184 {
3185 max_size = 3;
3186 connect =
3187 (fun () ->
3188 Repodb_postgresql.connect conninfo
3189 |> Result.map_error Repodb_postgresql.error_message);
3190 close = Repodb_postgresql.close;
3191 validate =
3192 Some
3193 (fun conn ->
3194 match Repodb_postgresql.exec conn "SELECT 1" ~params:[||] with
3195 | Ok () -> true
3196 | Error _ -> false);
3197 }
3198
3199let with_pool f () =
3200 let pool = Pool.create (pool_config ()) in
3201 (match Pool.acquire pool with
3202 | Ok conn ->
3203 let _ =
3204 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS pool_test" ~params:[||]
3205 in
3206 let _ =
3207 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS counter" ~params:[||]
3208 in
3209 Pool.release pool conn
3210 | Error _ -> ());
3211 Fun.protect
3212 ~finally:(fun () ->
3213 (match Pool.acquire pool with
3214 | Ok conn ->
3215 let _ =
3216 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS pool_test"
3217 ~params:[||]
3218 in
3219 let _ =
3220 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS counter"
3221 ~params:[||]
3222 in
3223 Pool.release pool conn
3224 | Error _ -> ());
3225 Pool.shutdown pool)
3226 (fun () -> f pool)
3227
3228let test_pool_acquire_release =
3229 with_pool (fun pool ->
3230 match Pool.acquire pool with
3231 | Error e -> Alcotest.fail (Pool.error_to_string e)
3232 | Ok conn ->
3233 (match Repodb_postgresql.exec conn "SELECT 1" ~params:[||] with
3234 | Ok () -> ()
3235 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e));
3236 Pool.release pool conn;
3237 Alcotest.(check int) "in_use after release" 0 (Pool.in_use pool))
3238
3239let test_pool_with_connection =
3240 with_pool (fun pool ->
3241 match Pool.acquire pool with
3242 | Error e -> Alcotest.fail (Pool.error_to_string e)
3243 | Ok conn -> (
3244 let _ =
3245 Repodb_postgresql.exec conn
3246 "CREATE TABLE pool_test (id SERIAL PRIMARY KEY, value TEXT)"
3247 ~params:[||]
3248 in
3249 Pool.release pool conn;
3250
3251 let result =
3252 Pool.with_connection pool (fun conn ->
3253 match
3254 Repodb_postgresql.exec conn
3255 "INSERT INTO pool_test (value) VALUES ($1)"
3256 ~params:[| Repodb.Driver.Value.text "test" |]
3257 with
3258 | Ok () -> "inserted"
3259 | Error e -> Repodb_postgresql.error_message e)
3260 in
3261 match result with
3262 | Ok "inserted" -> ()
3263 | Ok msg -> Alcotest.fail msg
3264 | Error e -> Alcotest.fail (Pool.error_to_string e)))
3265
3266let test_pool_max_size =
3267 with_pool (fun pool ->
3268 let c1 =
3269 match Pool.acquire pool with
3270 | Ok c -> c
3271 | Error e -> failwith (Pool.error_to_string e)
3272 in
3273 let c2 =
3274 match Pool.acquire pool with
3275 | Ok c -> c
3276 | Error e -> failwith (Pool.error_to_string e)
3277 in
3278 let c3 =
3279 match Pool.acquire pool with
3280 | Ok c -> c
3281 | Error e -> failwith (Pool.error_to_string e)
3282 in
3283 Alcotest.(check int) "3 in use" 3 (Pool.in_use pool);
3284 (match Pool.acquire pool with
3285 | Error Pool.Pool_empty -> ()
3286 | _ -> Alcotest.fail "Expected Pool_empty");
3287 Pool.release pool c1;
3288 Pool.release pool c2;
3289 Pool.release pool c3)
3290
3291let test_pool_reuse =
3292 with_pool (fun pool ->
3293 let c1 =
3294 match Pool.acquire pool with
3295 | Ok c -> c
3296 | Error e -> failwith (Pool.error_to_string e)
3297 in
3298 Pool.release pool c1;
3299 Alcotest.(check int) "1 available" 1 (Pool.available pool);
3300 let _c2 =
3301 match Pool.acquire pool with
3302 | Ok c -> c
3303 | Error e -> failwith (Pool.error_to_string e)
3304 in
3305 Alcotest.(check int) "still 1 total" 1 (Pool.size pool))
3306
3307let test_pool_validation () =
3308 let validation_count = Atomic.make 0 in
3309 let config =
3310 Pool.
3311 {
3312 max_size = 3;
3313 connect =
3314 (fun () ->
3315 Repodb_postgresql.connect conninfo
3316 |> Result.map_error Repodb_postgresql.error_message);
3317 close = Repodb_postgresql.close;
3318 validate =
3319 Some
3320 (fun conn ->
3321 Atomic.incr validation_count;
3322 match Repodb_postgresql.exec conn "SELECT 1" ~params:[||] with
3323 | Ok () -> true
3324 | Error _ -> false);
3325 }
3326 in
3327 let pool = Pool.create config in
3328 let c1 =
3329 match Pool.acquire pool with
3330 | Ok c -> c
3331 | Error e -> failwith (Pool.error_to_string e)
3332 in
3333 Pool.release pool c1;
3334 let _c2 =
3335 match Pool.acquire pool with
3336 | Ok c -> c
3337 | Error e -> failwith (Pool.error_to_string e)
3338 in
3339 Alcotest.(check bool)
3340 "validation was called" true
3341 (Atomic.get validation_count > 0);
3342 Pool.shutdown pool
3343
3344let test_pool_concurrent_domains () =
3345 let config =
3346 Pool.
3347 {
3348 max_size = 8;
3349 connect =
3350 (fun () ->
3351 Repodb_postgresql.connect conninfo
3352 |> Result.map_error Repodb_postgresql.error_message);
3353 close = Repodb_postgresql.close;
3354 validate = None;
3355 }
3356 in
3357 let pool = Pool.create config in
3358
3359 (match Pool.acquire pool with
3360 | Error e -> failwith (Pool.error_to_string e)
3361 | Ok conn ->
3362 let _ =
3363 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS counter" ~params:[||]
3364 in
3365 let _ =
3366 Repodb_postgresql.exec conn
3367 "CREATE TABLE counter (id INTEGER PRIMARY KEY, value INTEGER)"
3368 ~params:[||]
3369 in
3370 let _ =
3371 Repodb_postgresql.exec conn
3372 "INSERT INTO counter (id, value) VALUES (1, 0)" ~params:[||]
3373 in
3374 Pool.release pool conn);
3375
3376 let n_domains = 4 in
3377 let n_ops = 25 in
3378 let completed = Atomic.make 0 in
3379
3380 let domain_work () =
3381 for _ = 1 to n_ops do
3382 match Pool.acquire pool with
3383 | Ok conn ->
3384 let _ =
3385 Repodb_postgresql.exec conn
3386 "UPDATE counter SET value = value + 1 WHERE id = 1" ~params:[||]
3387 in
3388 Pool.release pool conn
3389 | Error e -> failwith (Pool.error_to_string e)
3390 done;
3391 Atomic.incr completed
3392 in
3393
3394 let domains = List.init n_domains (fun _ -> Domain.spawn domain_work) in
3395 List.iter Domain.join domains;
3396
3397 Alcotest.(check int) "all domains completed" n_domains (Atomic.get completed);
3398
3399 let final =
3400 match Pool.acquire pool with
3401 | Ok conn ->
3402 (match
3403 Repodb_postgresql.query conn "SELECT value FROM counter WHERE id = 1"
3404 ~params:[||]
3405 with
3406 | Ok [ row ] -> Repodb.Driver.row_int row 0
3407 | _ -> failwith "Query failed")
3408 |> fun v ->
3409 Pool.release pool conn;
3410 v
3411 | Error e -> failwith (Pool.error_to_string e)
3412 in
3413
3414 Alcotest.(check int) "all increments" (n_domains * n_ops) final;
3415
3416 (match Pool.acquire pool with
3417 | Ok conn ->
3418 let _ =
3419 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS counter" ~params:[||]
3420 in
3421 Pool.release pool conn
3422 | Error _ -> ());
3423 Pool.shutdown pool
3424
3425let pool_tests =
3426 [
3427 ("acquire_release", `Quick, test_pool_acquire_release);
3428 ("with_connection", `Quick, test_pool_with_connection);
3429 ("max_size", `Quick, test_pool_max_size);
3430 ("reuse", `Quick, test_pool_reuse);
3431 ("validation", `Quick, test_pool_validation);
3432 ("concurrent_domains", `Slow, test_pool_concurrent_domains);
3433 ]
3434
3435let test_date_insert_and_query =
3436 with_db (fun conn ->
3437 let _ =
3438 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS events" ~params:[||]
3439 in
3440 let _ =
3441 Repodb_postgresql.exec conn
3442 "CREATE TABLE events (id SERIAL PRIMARY KEY, name TEXT, event_date \
3443 DATE)"
3444 ~params:[||]
3445 in
3446 let date : Ptime.date = (2024, 6, 15) in
3447 let date_value = Repodb.Types.to_value Repodb.Types.pdate date in
3448 let insert =
3449 Repodb_postgresql.exec conn
3450 "INSERT INTO events (name, event_date) VALUES ($1, $2)"
3451 ~params:[| Repodb.Driver.Value.text "Conference"; date_value |]
3452 in
3453 (match insert with
3454 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3455 | Ok () -> ());
3456 match
3457 Repodb_postgresql.query conn
3458 "SELECT event_date FROM events WHERE name = $1"
3459 ~params:[| Repodb.Driver.Value.text "Conference" |]
3460 with
3461 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3462 | Ok [] -> Alcotest.fail "expected row"
3463 | Ok (row :: _) -> (
3464 let raw_value = Repodb.Driver.row_get_idx row 0 in
3465 match Repodb.Types.of_value Repodb.Types.pdate raw_value with
3466 | Error e -> Alcotest.fail ("failed to decode date: " ^ e)
3467 | Ok (y, m, d) ->
3468 Alcotest.(check int) "year" 2024 y;
3469 Alcotest.(check int) "month" 6 m;
3470 Alcotest.(check int) "day" 15 d))
3471
3472let test_date_comparison =
3473 with_db (fun conn ->
3474 let _ =
3475 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS events" ~params:[||]
3476 in
3477 let _ =
3478 Repodb_postgresql.exec conn
3479 "CREATE TABLE events (id SERIAL PRIMARY KEY, name TEXT, event_date \
3480 DATE)"
3481 ~params:[||]
3482 in
3483 let dates =
3484 [
3485 ((2024, 1, 10), "Event A");
3486 ((2024, 6, 15), "Event B");
3487 ((2024, 12, 25), "Event C");
3488 ]
3489 in
3490 List.iter
3491 (fun (date, name) ->
3492 let date_value = Repodb.Types.to_value Repodb.Types.pdate date in
3493 let _ =
3494 Repodb_postgresql.exec conn
3495 "INSERT INTO events (name, event_date) VALUES ($1, $2)"
3496 ~params:[| Repodb.Driver.Value.text name; date_value |]
3497 in
3498 ())
3499 dates;
3500 match
3501 Repodb_postgresql.query conn
3502 "SELECT name FROM events WHERE event_date > $1 ORDER BY event_date"
3503 ~params:[| Repodb.Driver.Value.text "2024-06-01" |]
3504 with
3505 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3506 | Ok rows ->
3507 Alcotest.(check int) "two events after June 1" 2 (List.length rows);
3508 Alcotest.(check string)
3509 "first is Event B" "Event B"
3510 (Repodb.Driver.row_text (List.nth rows 0) 0);
3511 Alcotest.(check string)
3512 "second is Event C" "Event C"
3513 (Repodb.Driver.row_text (List.nth rows 1) 0))
3514
3515let test_date_null_handling =
3516 with_db (fun conn ->
3517 let _ =
3518 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS events" ~params:[||]
3519 in
3520 let _ =
3521 Repodb_postgresql.exec conn
3522 "CREATE TABLE events (id SERIAL PRIMARY KEY, name TEXT, event_date \
3523 DATE)"
3524 ~params:[||]
3525 in
3526 let _ =
3527 Repodb_postgresql.exec conn
3528 "INSERT INTO events (name, event_date) VALUES ($1, $2)"
3529 ~params:
3530 [| Repodb.Driver.Value.text "TBD Event"; Repodb.Driver.Value.null |]
3531 in
3532 match
3533 Repodb_postgresql.query_one conn
3534 "SELECT event_date FROM events WHERE name = $1"
3535 ~params:[| Repodb.Driver.Value.text "TBD Event" |]
3536 with
3537 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3538 | Ok None -> Alcotest.fail "expected row"
3539 | Ok (Some row) -> (
3540 let raw_value = Repodb.Driver.row_get_idx row 0 in
3541 match
3542 Repodb.Types.of_value
3543 (Repodb.Types.option Repodb.Types.pdate)
3544 raw_value
3545 with
3546 | Error e -> Alcotest.fail ("failed to decode: " ^ e)
3547 | Ok None -> ()
3548 | Ok (Some _) -> Alcotest.fail "expected None for NULL date"))
3549
3550let test_date_roundtrip =
3551 with_db (fun conn ->
3552 let _ =
3553 Repodb_postgresql.exec conn "DROP TABLE IF EXISTS events" ~params:[||]
3554 in
3555 let _ =
3556 Repodb_postgresql.exec conn
3557 "CREATE TABLE events (id SERIAL PRIMARY KEY, event_date DATE)"
3558 ~params:[||]
3559 in
3560 let test_dates =
3561 [ (2024, 1, 1); (2024, 12, 31); (1999, 6, 15); (2030, 2, 28) ]
3562 in
3563 List.iter
3564 (fun date ->
3565 let date_value = Repodb.Types.to_value Repodb.Types.pdate date in
3566 let _ =
3567 Repodb_postgresql.exec conn
3568 "INSERT INTO events (event_date) VALUES ($1)"
3569 ~params:[| date_value |]
3570 in
3571 ())
3572 test_dates;
3573 match
3574 Repodb_postgresql.query conn "SELECT event_date FROM events ORDER BY id"
3575 ~params:[||]
3576 with
3577 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3578 | Ok rows ->
3579 List.iter2
3580 (fun expected_date row ->
3581 let raw_value = Repodb.Driver.row_get_idx row 0 in
3582 match Repodb.Types.of_value Repodb.Types.pdate raw_value with
3583 | Error e -> Alcotest.fail ("decode failed: " ^ e)
3584 | Ok actual_date ->
3585 Alcotest.(check (triple int int int))
3586 "date roundtrip" expected_date actual_date)
3587 test_dates rows)
3588
3589let test_date_current_date =
3590 with_db (fun conn ->
3591 match
3592 Repodb_postgresql.query_one conn "SELECT CURRENT_DATE" ~params:[||]
3593 with
3594 | Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3595 | Ok None -> Alcotest.fail "expected row"
3596 | Ok (Some row) -> (
3597 let raw_value = Repodb.Driver.row_get_idx row 0 in
3598 match Repodb.Types.of_value Repodb.Types.pdate raw_value with
3599 | Error e -> Alcotest.fail ("failed to decode CURRENT_DATE: " ^ e)
3600 | Ok (y, m, d) ->
3601 Alcotest.(check bool)
3602 "year reasonable" true
3603 (y >= 2024 && y <= 2100);
3604 Alcotest.(check bool) "month valid" true (m >= 1 && m <= 12);
3605 Alcotest.(check bool) "day valid" true (d >= 1 && d <= 31)))
3606
3607let date_tests =
3608 [
3609 ("insert_and_query", `Quick, test_date_insert_and_query);
3610 ("comparison", `Quick, test_date_comparison);
3611 ("null_handling", `Quick, test_date_null_handling);
3612 ("roundtrip", `Quick, test_date_roundtrip);
3613 ("current_date", `Quick, test_date_current_date);
3614 ]
3615
3616let () =
3617 Alcotest.run "repodb-postgresql"
3618 [
3619 ("Basic", basic_tests);
3620 ("Indexes", index_tests);
3621 ("Relations", relation_tests);
3622 ("Joins", join_tests);
3623 ("Queries", query_tests);
3624 ("Advanced", advanced_tests);
3625 ("Streaming", streaming_tests);
3626 ("Errors", error_tests);
3627 ("Preload", preload_tests);
3628 ("Multi", multi_tests);
3629 ("Query-Repo", query_repo_tests);
3630 ("Pool", pool_tests);
3631 ("Date", date_tests);
3632 ]