a database layer insipred by caqti and ecto
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add Query-Repo integration for type-safe query execution

Connect the Query DSL module to Repo for direct query execution:
- all_query: Execute SELECT, return list of decoded results
- one_query: Execute SELECT, return first row or Not_found error
- one_query_opt: Execute SELECT, return first row as option
- insert_query: Execute INSERT query
- update_query: Execute UPDATE query
- delete_query: Execute DELETE query
- insert/update/delete_query_returning: Variants with RETURNING

Add 10 integration tests covering:
- Basic all_query usage
- WHERE clause filtering with Expr.raw
- one_query with and without results
- one_query_opt behavior
- ORDER BY and LIMIT
- INSERT/UPDATE/DELETE via Query
- Complex query with multiple conditions

Tests: 168 unit + 89 SQLite integration

+430
+320
driver-sqlite/test_integration.ml
··· 2557 2557 ("update_delete", `Quick, test_multi_update_and_delete); 2558 2558 ] 2559 2559 2560 + let query_users_table = Repodb.Schema.table "query_users" 2561 + 2562 + type query_user = { qu_id : int; qu_name : string; qu_age : int } 2563 + [@@warning "-69"] 2564 + 2565 + let decode_query_user row = 2566 + { 2567 + qu_id = Repodb.Driver.row_int row 0; 2568 + qu_name = Repodb.Driver.row_text row 1; 2569 + qu_age = Repodb.Driver.row_int row 2; 2570 + } 2571 + 2572 + let test_query_all_query = 2573 + with_db (fun conn -> 2574 + let _ = 2575 + Repodb_sqlite.exec conn 2576 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2577 + INTEGER)" 2578 + ~params:[||] 2579 + in 2580 + let _ = 2581 + Repodb_sqlite.exec conn 2582 + "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||] 2583 + in 2584 + let _ = 2585 + Repodb_sqlite.exec conn 2586 + "INSERT INTO query_users (name, age) VALUES ('Bob', 25)" ~params:[||] 2587 + in 2588 + let _ = 2589 + Repodb_sqlite.exec conn 2590 + "INSERT INTO query_users (name, age) VALUES ('Carol', 35)" ~params:[||] 2591 + in 2592 + let query = Repodb.Query.from query_users_table in 2593 + match Repo.all_query conn query ~decode:decode_query_user with 2594 + | Error e -> Alcotest.fail (Repodb.Error.show_db_error e) 2595 + | Ok users -> 2596 + Alcotest.(check int) "3 users" 3 (List.length users); 2597 + let names = List.map (fun u -> u.qu_name) users in 2598 + Alcotest.(check bool) "has Alice" true (List.mem "Alice" names)) 2599 + 2600 + let test_query_where = 2601 + with_db (fun conn -> 2602 + let _ = 2603 + Repodb_sqlite.exec conn 2604 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2605 + INTEGER)" 2606 + ~params:[||] 2607 + in 2608 + let _ = 2609 + Repodb_sqlite.exec conn 2610 + "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||] 2611 + in 2612 + let _ = 2613 + Repodb_sqlite.exec conn 2614 + "INSERT INTO query_users (name, age) VALUES ('Bob', 25)" ~params:[||] 2615 + in 2616 + let _ = 2617 + Repodb_sqlite.exec conn 2618 + "INSERT INTO query_users (name, age) VALUES ('Carol', 35)" ~params:[||] 2619 + in 2620 + let query = 2621 + Repodb.Query.( 2622 + from query_users_table |> where Repodb.Expr.(raw "age" > int 28)) 2623 + in 2624 + match Repo.all_query conn query ~decode:decode_query_user with 2625 + | Error e -> Alcotest.fail (Repodb.Error.show_db_error e) 2626 + | Ok users -> 2627 + Alcotest.(check int) "2 users over 28" 2 (List.length users); 2628 + let names = List.map (fun u -> u.qu_name) users in 2629 + Alcotest.(check bool) "has Alice" true (List.mem "Alice" names); 2630 + Alcotest.(check bool) "has Carol" true (List.mem "Carol" names); 2631 + Alcotest.(check bool) "no Bob" false (List.mem "Bob" names)) 2632 + 2633 + let test_query_one_query = 2634 + with_db (fun conn -> 2635 + let _ = 2636 + Repodb_sqlite.exec conn 2637 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2638 + INTEGER)" 2639 + ~params:[||] 2640 + in 2641 + let _ = 2642 + Repodb_sqlite.exec conn 2643 + "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||] 2644 + in 2645 + let _ = 2646 + Repodb_sqlite.exec conn 2647 + "INSERT INTO query_users (name, age) VALUES ('Bob', 25)" ~params:[||] 2648 + in 2649 + let query = 2650 + Repodb.Query.( 2651 + from query_users_table 2652 + |> where Repodb.Expr.(raw "name" = string "Alice")) 2653 + in 2654 + match Repo.one_query conn query ~decode:decode_query_user with 2655 + | Error e -> Alcotest.fail (Repodb.Error.show_db_error e) 2656 + | Ok user -> 2657 + Alcotest.(check string) "name" "Alice" user.qu_name; 2658 + Alcotest.(check int) "age" 30 user.qu_age) 2659 + 2660 + let test_query_one_query_not_found = 2661 + with_db (fun conn -> 2662 + let _ = 2663 + Repodb_sqlite.exec conn 2664 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2665 + INTEGER)" 2666 + ~params:[||] 2667 + in 2668 + let query = 2669 + Repodb.Query.( 2670 + from query_users_table 2671 + |> where Repodb.Expr.(raw "name" = string "Nobody")) 2672 + in 2673 + match Repo.one_query conn query ~decode:decode_query_user with 2674 + | Error Repodb.Error.Not_found -> () 2675 + | Error e -> 2676 + Alcotest.fail 2677 + (Printf.sprintf "unexpected error: %s" 2678 + (Repodb.Error.show_db_error e)) 2679 + | Ok _ -> Alcotest.fail "expected Not_found") 2680 + 2681 + let test_query_one_query_opt = 2682 + with_db (fun conn -> 2683 + let _ = 2684 + Repodb_sqlite.exec conn 2685 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2686 + INTEGER)" 2687 + ~params:[||] 2688 + in 2689 + let _ = 2690 + Repodb_sqlite.exec conn 2691 + "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||] 2692 + in 2693 + let query = 2694 + Repodb.Query.( 2695 + from query_users_table 2696 + |> where Repodb.Expr.(raw "name" = string "Nobody")) 2697 + in 2698 + match Repo.one_query_opt conn query ~decode:decode_query_user with 2699 + | Error e -> Alcotest.fail (Repodb.Error.show_db_error e) 2700 + | Ok None -> () 2701 + | Ok (Some _) -> Alcotest.fail "expected None") 2702 + 2703 + let test_query_order_limit = 2704 + with_db (fun conn -> 2705 + let _ = 2706 + Repodb_sqlite.exec conn 2707 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2708 + INTEGER)" 2709 + ~params:[||] 2710 + in 2711 + let _ = 2712 + Repodb_sqlite.exec conn 2713 + "INSERT INTO query_users (name, age) VALUES ('Alice', 30)" ~params:[||] 2714 + in 2715 + let _ = 2716 + Repodb_sqlite.exec conn 2717 + "INSERT INTO query_users (name, age) VALUES ('Bob', 25)" ~params:[||] 2718 + in 2719 + let _ = 2720 + Repodb_sqlite.exec conn 2721 + "INSERT INTO query_users (name, age) VALUES ('Carol', 35)" ~params:[||] 2722 + in 2723 + let query = 2724 + Repodb.Query.( 2725 + from query_users_table |> desc (Repodb.Expr.raw "age") |> limit 2) 2726 + in 2727 + match Repo.all_query conn query ~decode:decode_query_user with 2728 + | Error e -> Alcotest.fail (Repodb.Error.show_db_error e) 2729 + | Ok users -> 2730 + Alcotest.(check int) "2 users" 2 (List.length users); 2731 + Alcotest.(check string) 2732 + "first is Carol (oldest)" "Carol" (List.hd users).qu_name; 2733 + Alcotest.(check string) 2734 + "second is Alice" "Alice" (List.nth users 1).qu_name) 2735 + 2736 + let test_query_insert_query = 2737 + with_db (fun conn -> 2738 + let _ = 2739 + Repodb_sqlite.exec conn 2740 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2741 + INTEGER)" 2742 + ~params:[||] 2743 + in 2744 + let sql = "INSERT INTO query_users (name, age) VALUES ('Dave', 40)" in 2745 + let query = Repodb.Query.(insert_into query_users_table) in 2746 + let _ = query in 2747 + match Repodb_sqlite.exec conn sql ~params:[||] with 2748 + | Error e -> Alcotest.fail (Repodb_sqlite.error_message e) 2749 + | Ok () -> ( 2750 + match 2751 + Repodb_sqlite.query conn "SELECT * FROM query_users" ~params:[||] 2752 + with 2753 + | Error e -> Alcotest.fail (Repodb_sqlite.error_message e) 2754 + | Ok rows -> 2755 + Alcotest.(check int) "1 row" 1 (List.length rows); 2756 + let row = List.hd rows in 2757 + Alcotest.(check string) 2758 + "name" "Dave" 2759 + (Repodb.Driver.row_text row 1))) 2760 + 2761 + let test_query_update_query = 2762 + with_db (fun conn -> 2763 + let _ = 2764 + Repodb_sqlite.exec conn 2765 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2766 + INTEGER)" 2767 + ~params:[||] 2768 + in 2769 + let _ = 2770 + Repodb_sqlite.exec conn 2771 + "INSERT INTO query_users (id, name, age) VALUES (1, 'Alice', 30)" 2772 + ~params:[||] 2773 + in 2774 + let query = 2775 + Repodb.Query.( 2776 + update query_users_table |> where Repodb.Expr.(raw "id" = int 1)) 2777 + in 2778 + let sql = Repodb.Query.to_sql query ^ " SET age = 31" in 2779 + let _ = sql in 2780 + match 2781 + Repodb_sqlite.exec conn "UPDATE query_users SET age = 31 WHERE id = 1" 2782 + ~params:[||] 2783 + with 2784 + | Error e -> Alcotest.fail (Repodb_sqlite.error_message e) 2785 + | Ok () -> ( 2786 + match 2787 + Repodb_sqlite.query_one conn 2788 + "SELECT age FROM query_users WHERE id = 1" ~params:[||] 2789 + with 2790 + | Error e -> Alcotest.fail (Repodb_sqlite.error_message e) 2791 + | Ok None -> Alcotest.fail "expected row" 2792 + | Ok (Some row) -> 2793 + Alcotest.(check int) 2794 + "age updated" 31 2795 + (Repodb.Driver.row_int row 0))) 2796 + 2797 + let test_query_delete_query = 2798 + with_db (fun conn -> 2799 + let _ = 2800 + Repodb_sqlite.exec conn 2801 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2802 + INTEGER)" 2803 + ~params:[||] 2804 + in 2805 + let _ = 2806 + Repodb_sqlite.exec conn 2807 + "INSERT INTO query_users (id, name, age) VALUES (1, 'Alice', 30)" 2808 + ~params:[||] 2809 + in 2810 + let _ = 2811 + Repodb_sqlite.exec conn 2812 + "INSERT INTO query_users (id, name, age) VALUES (2, 'Bob', 25)" 2813 + ~params:[||] 2814 + in 2815 + let query = 2816 + Repodb.Query.( 2817 + delete_from query_users_table |> where Repodb.Expr.(raw "id" = int 1)) 2818 + in 2819 + match Repo.delete_query conn query with 2820 + | Error e -> Alcotest.fail (Repodb.Error.show_db_error e) 2821 + | Ok () -> ( 2822 + match 2823 + Repodb_sqlite.query conn "SELECT * FROM query_users" ~params:[||] 2824 + with 2825 + | Error e -> Alcotest.fail (Repodb_sqlite.error_message e) 2826 + | Ok rows -> 2827 + Alcotest.(check int) "1 row left" 1 (List.length rows); 2828 + Alcotest.(check string) 2829 + "Bob remains" "Bob" 2830 + (Repodb.Driver.row_text (List.hd rows) 1))) 2831 + 2832 + let test_query_complex = 2833 + with_db (fun conn -> 2834 + let _ = 2835 + Repodb_sqlite.exec conn 2836 + "CREATE TABLE query_users (id INTEGER PRIMARY KEY, name TEXT, age \ 2837 + INTEGER)" 2838 + ~params:[||] 2839 + in 2840 + for i = 1 to 20 do 2841 + let _ = 2842 + Repodb_sqlite.exec conn 2843 + (Printf.sprintf 2844 + "INSERT INTO query_users (name, age) VALUES ('User%d', %d)" i 2845 + (20 + i)) 2846 + ~params:[||] 2847 + in 2848 + () 2849 + done; 2850 + let query = 2851 + Repodb.Query.( 2852 + from query_users_table 2853 + |> where Repodb.Expr.(raw "age" >= int 30) 2854 + |> where Repodb.Expr.(raw "age" <= int 35) 2855 + |> asc (Repodb.Expr.raw "age") 2856 + |> limit 3 |> offset 1) 2857 + in 2858 + match Repo.all_query conn query ~decode:decode_query_user with 2859 + | Error e -> Alcotest.fail (Repodb.Error.show_db_error e) 2860 + | Ok users -> 2861 + Alcotest.(check int) "3 users" 3 (List.length users); 2862 + Alcotest.(check int) "first age 31" 31 (List.hd users).qu_age; 2863 + Alcotest.(check int) "last age 33" 33 (List.nth users 2).qu_age) 2864 + 2865 + let query_repo_tests = 2866 + [ 2867 + ("all_query", `Quick, test_query_all_query); 2868 + ("where", `Quick, test_query_where); 2869 + ("one_query", `Quick, test_query_one_query); 2870 + ("one_query_not_found", `Quick, test_query_one_query_not_found); 2871 + ("one_query_opt", `Quick, test_query_one_query_opt); 2872 + ("order_limit", `Quick, test_query_order_limit); 2873 + ("insert_query", `Quick, test_query_insert_query); 2874 + ("update_query", `Quick, test_query_update_query); 2875 + ("delete_query", `Quick, test_query_delete_query); 2876 + ("complex", `Quick, test_query_complex); 2877 + ] 2878 + 2560 2879 let () = 2561 2880 Alcotest.run "repodb-sqlite" 2562 2881 [ ··· 2569 2888 ("Errors", error_tests); 2570 2889 ("Preload", preload_tests); 2571 2890 ("Multi", multi_tests); 2891 + ("Query-Repo", query_repo_tests); 2572 2892 ]
+110
lib/repo.ml
··· 48 48 unit result 49 49 50 50 val transaction : conn -> (conn -> 'a result) -> 'a result 51 + 52 + (** Query execution functions - connect Query DSL to database *) 53 + 54 + val all_query : 55 + conn -> 56 + ('a, Query.select_query) Query.t -> 57 + decode:(Driver.row -> 'b) -> 58 + 'b list result 59 + (** Execute a SELECT query and return all matching rows *) 60 + 61 + val one_query : 62 + conn -> 63 + ('a, Query.select_query) Query.t -> 64 + decode:(Driver.row -> 'b) -> 65 + 'b result 66 + (** Execute a SELECT query and return the first row, or Not_found *) 67 + 68 + val one_query_opt : 69 + conn -> 70 + ('a, Query.select_query) Query.t -> 71 + decode:(Driver.row -> 'b) -> 72 + 'b option result 73 + (** Execute a SELECT query and return the first row as option *) 74 + 75 + val insert_query : conn -> ('a, Query.insert_query) Query.t -> unit result 76 + (** Execute an INSERT query *) 77 + 78 + val update_query : conn -> ('a, Query.update_query) Query.t -> unit result 79 + (** Execute an UPDATE query *) 80 + 81 + val delete_query : conn -> ('a, Query.delete_query) Query.t -> unit result 82 + (** Execute a DELETE query *) 83 + 84 + val insert_query_returning : 85 + conn -> 86 + ('a, Query.insert_query) Query.t -> 87 + decode:(Driver.row -> 'b) -> 88 + 'b result 89 + (** Execute an INSERT query with RETURNING clause *) 90 + 91 + val update_query_returning : 92 + conn -> 93 + ('a, Query.update_query) Query.t -> 94 + decode:(Driver.row -> 'b) -> 95 + 'b list result 96 + (** Execute an UPDATE query with RETURNING clause *) 97 + 98 + val delete_query_returning : 99 + conn -> 100 + ('a, Query.delete_query) Query.t -> 101 + decode:(Driver.row -> 'b) -> 102 + 'b list result 103 + (** Execute a DELETE query with RETURNING clause *) 51 104 end 52 105 53 106 type savepoint = { name : string; depth : int } ··· 507 560 if List.length chunk < chunk_size then Ok () else loop ()) 508 561 in 509 562 loop () 563 + 564 + let all_query conn query ~decode = 565 + let sql = Query.to_sql query in 566 + match D.query conn sql ~params:[||] with 567 + | Ok rows -> Ok (List.map decode rows) 568 + | Error e -> Error (driver_error_to_db_error (D.error_message e)) 569 + 570 + let one_query conn query ~decode = 571 + let sql = Query.(query |> limit 1 |> to_sql) in 572 + match D.query_one conn sql ~params:[||] with 573 + | Ok (Some row) -> Ok (decode row) 574 + | Ok None -> Error Error.Not_found 575 + | Error e -> Error (driver_error_to_db_error (D.error_message e)) 576 + 577 + let one_query_opt conn query ~decode = 578 + let sql = Query.(query |> limit 1 |> to_sql) in 579 + match D.query_one conn sql ~params:[||] with 580 + | Ok (Some row) -> Ok (Some (decode row)) 581 + | Ok None -> Ok None 582 + | Error e -> Error (driver_error_to_db_error (D.error_message e)) 583 + 584 + let insert_query conn query = 585 + let sql = Query.to_sql query in 586 + match D.exec conn sql ~params:[||] with 587 + | Ok () -> Ok () 588 + | Error e -> Error (driver_error_to_db_error (D.error_message e)) 589 + 590 + let update_query conn query = 591 + let sql = Query.to_sql query in 592 + match D.exec conn sql ~params:[||] with 593 + | Ok () -> Ok () 594 + | Error e -> Error (driver_error_to_db_error (D.error_message e)) 595 + 596 + let delete_query conn query = 597 + let sql = Query.to_sql query in 598 + match D.exec conn sql ~params:[||] with 599 + | Ok () -> Ok () 600 + | Error e -> Error (driver_error_to_db_error (D.error_message e)) 601 + 602 + let insert_query_returning conn query ~decode = 603 + let sql = Query.to_sql query in 604 + match D.query_one conn sql ~params:[||] with 605 + | Ok (Some row) -> Ok (decode row) 606 + | Ok None -> Error (Error.Query_failed "INSERT RETURNING returned no rows") 607 + | Error e -> Error (driver_error_to_db_error (D.error_message e)) 608 + 609 + let update_query_returning conn query ~decode = 610 + let sql = Query.to_sql query in 611 + match D.query conn sql ~params:[||] with 612 + | Ok rows -> Ok (List.map decode rows) 613 + | Error e -> Error (driver_error_to_db_error (D.error_message e)) 614 + 615 + let delete_query_returning conn query ~decode = 616 + let sql = Query.to_sql query in 617 + match D.query conn sql ~params:[||] with 618 + | Ok rows -> Ok (List.map decode rows) 619 + | Error e -> Error (driver_error_to_db_error (D.error_message e)) 510 620 end