a database layer insipred by caqti and ecto
at main 128 kB view raw
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 ]