From b91eed16db63e834a4617e59af62282f1918f075 Mon Sep 17 00:00:00 2001 From: Cuihtlauac Alvarado Date: Mon, 28 Oct 2024 08:43:17 +0100 Subject: [PATCH] (data) Cookbook database recipes (#2376) * Minimize task.yml * line editing * update for clarity * editing ppx_rapper recipe * minor correction 00-caqti-ppx-rapper.ml * editing ezsqlite recipe --------- Co-authored-by: Cuihtlauac ALVARADO Co-authored-by: Christine Rose Co-authored-by: sabine <6594573+sabine@users.noreply.github.com> --- .../00-caqti-ppx-rapper.ml | 184 ++++++++++++++++++ .../01-ezsqlite.ml | 76 ++++++++ 2 files changed, 260 insertions(+) create mode 100644 data/cookbook/sqlite-create-insert-select/00-caqti-ppx-rapper.ml create mode 100644 data/cookbook/sqlite-create-insert-select/01-ezsqlite.ml diff --git a/data/cookbook/sqlite-create-insert-select/00-caqti-ppx-rapper.ml b/data/cookbook/sqlite-create-insert-select/00-caqti-ppx-rapper.ml new file mode 100644 index 0000000000..ca287d8fc6 --- /dev/null +++ b/data/cookbook/sqlite-create-insert-select/00-caqti-ppx-rapper.ml @@ -0,0 +1,184 @@ +--- +packages: +- name: "ppx_rapper_lwt" + tested_version: "3.1.0" + used_libraries: + - ppx_rapper_lwt +- name: "ppx_rapper" + tested_version: "3.1.0" + used_libraries: + - ppx_rapper +- name: "caqti-driver-sqlite3" + tested_version: "1.9.0" + used_libraries: + - caqti-driver-sqlite3 +- name: "caqti-lwt" + tested_version: "1.9.0" + used_libraries: + - caqti-lwt +- name: "lwt" + tested_version: "5.7.0" + used_libraries: + - lwt + - lwt.unix +discussion: | + The `Caqti` library permits portable programming + with SQLite, MariaDB, and PostgreSQL. + `ppx_rapper` converts annotated SQL strings into `Caqti` queries. + This preprocessor makes all type conversions transparent and leverages OCaml's strong typing. + It also checks the SQL syntax of the given query. + See [the `Caqti` reference page](https://github.com/paurkedal/ocaml-caqti) + and [the `ppx_rapper` reference page](https://github.com/roddyyaga/ppx_rapper). +--- + +(* The `Caqti/ppx_rapper` combo uses an Lwt environment. + Let operators `( let* )` and `( let*? )` are defined as usual for Lwt, to have a clean + notation for chaining promises. `( let*? )` extracts the result from a returned `Ok result` or + stops the execution in case of an `Error err` value. +*) +let ( let* ) = Lwt.bind +let ( let*? ) = Lwt_result.bind + +(* The helper function `iter_queries` sequentially schedules a list of queries. + Each query is a function that takes the + connection handle of the database as an argument. *) +let iter_queries queries connection = + List.fold_left + (fun a f -> + Lwt_result.bind a (fun () -> f connection)) + (Lwt.return (Ok ())) + queries + +(* The `%rapper` node here makes `ppx_rapper` generate code, such that, when applying + the `create_employees_table () connection` function, + the provided SQL `CREATE` query will be run without any parameters and without + receiving any data from the database. + + In case of successful execution of the query, we get back an `Ok ()` value, otherwise + we get an `Error` value. + *) +let create_employees_table = + [%rapper + execute {sql| CREATE TABLE employees + (name VARCHAR, + firstname VARCHAR, + age INTEGER) + |sql} + ] + +type employee = + { name:string; firstname:string; age:int } +let employees = [ + {name = "Dupont"; firstname = "Jacques"; age = 36}; + {name = "Legendre"; firstname = "Patrick"; age = 42} + ] + +(* For the SQL `INSERT` query, `ppx_rapper` generates a function `insert_employee (p: employee) connection`. + The tag `record_in` tag tells `ppx_rapper` to read the values `name`, `firstname`, + and `age` from the provided record value, while the `%[TYPE_NAME]{[INPUT_FIELD_NAME]}` notation specifies + which conversions to perform on the input values. *) +let insert_employee = + [%rapper + execute + {sql| INSERT INTO employees VALUES + (%string{name}, + %string{firstname}, + %int{age}) + |sql} + record_in + ] + +(* The `get_many` tag makes `ppx_rapper` generate code that queries the database and + receives a list of values. The `record_out` tag specifies that each list item + will be a record. + + The `@[TYPE_NAME]{[COLUMN_NAME]}` notation specifies + which conversions to perform on the output values. + *) +let get_all_employees = + [%rapper + get_many + {sql|SELECT + @string{name}, + @string{firstname}, + @int{age} + FROM employees + |sql} + record_out + ] + +(* Here's another example query that selects a single row via the SQL `WHERE` clause, using the `get_opt` tag. + This query has both input (`name`) and output values (`name`, `firstname`, `age`). + + Here the absence of the `record_in` tag makes `ppx_rapper` generate code where the + input values are passed as named arguments. + The `get_opt` tag means that the result will be an option: `None` if no rows matching the criteria + is found, and `Some r` if a row match the criteria. + *) +let get_employee_by_name = + [%rapper + get_opt + {sql|SELECT + @string{name}, + @string{firstname}, + @int{age} + FROM employees + WHERE name=%string{name} + |sql} + record_out + ] + + +(* All query functions generated by `ppx_rapper` take an argument and a `connection` parameter. +The function `insert_employee` must be called with +a value of type `employee` and `connection`. To insert multiple records from +a list, we use `List.map` to create a list +of functions. Each of these functions will execute its +associated query when called. The function `iter_queries` runs +the queries in sequence. + +Note that, if you have to insert many records, it makes sense to perform a bulk insert query instead. *) +let execute_queries connection = + let*? () = create_employees_table () connection in + let*? () = + iter_queries + (List.map insert_employee employees) + connection + in + let*? employees = get_all_employees () connection in + employees |> List.iter (fun employees -> + Printf.printf + "name=%s, firstname=%s, age=%d\n" + employees.name + employees.firstname + employees.age); + let*? employees = + get_employee_by_name ~name:"Dupont" connection + in + match employees with + | Some employees' -> + Printf.printf + "found:name=%s, firstname=%s, age=%d\n" + employees'.name + employees'.firstname + employees'.age; + Lwt_result.return () + | None -> + print_string "Not found"; + Lwt_result.return () + +(* The main program starts by establishing an Lwt environment. + The function `with_connection` opens the database, + executes a function with the `connection` database handle, + and closes the database connection again, even when an exception is raised. *) +let () = + match Lwt_main.run @@ + Caqti_lwt.with_connection + (Uri.of_string "sqlite3:essai.sqlite") + execute_queries + with + | Result.Ok () -> + print_string "OK\n" + | Result.Error err -> + print_string (Caqti_error.show err) + diff --git a/data/cookbook/sqlite-create-insert-select/01-ezsqlite.ml b/data/cookbook/sqlite-create-insert-select/01-ezsqlite.ml new file mode 100644 index 0000000000..544ada998a --- /dev/null +++ b/data/cookbook/sqlite-create-insert-select/01-ezsqlite.ml @@ -0,0 +1,76 @@ +--- +packages: +- name: "ezsqlite" + tested_version: "0.4.2" + used_libraries: + - ezsqlite +--- + +(* Open or create (if it doesn't exist) the SQLite database *) +let db = Ezsqlite.load "employees.sqlite" + +(* Create the `employees` table. + The function `run_ign` ("run and ignore") executes the query and discards the database response. + *) +let () = + Ezsqlite.run_ign db + "CREATE TABLE employees ( + name VARCHAR NOT NULL, + firstname VARCHAR NOT NULL, + age INTEGER NOT NULL + )" + () + +type employee = + { name:string; firstname:string; age:int } + +let employees = [ + {name = "Dupont"; firstname = "Jacques"; age = 36}; + {name = "Legendre"; firstname = "Patrick"; age = 42} + ] + +let () = +(* The function `Ezsqlite.prepare` prepares the statement, so that later actual values + can be bound to the variables `:name`, `:firstname`, and `:age`. *) + let stmt = Ezsqlite.prepare db + {| + INSERT into employees + VALUES (:name, :firstname, :age) + |} + in +(* Running these `Ezsqlite` functions in sequence binds the values from the + `employee` record and executes the query. *) + let insert_employee (employee: employee) = + Ezsqlite.clear stmt; + Ezsqlite.bind_dict stmt + [":name", Ezsqlite.Value.Text employee.name; + ":firstname", Ezsqlite.Value.Text + employee.firstname; + ":age", Ezsqlite.Value.Integer + (Int64.of_int employee.age)]; + Ezsqlite.exec stmt + in + employees + |> List.iter insert_employee + +(* The `iter` function executes a query and then maps a given function over all + rows returned by the database. + *) +let () = + let stmt = Ezsqlite.prepare db + "SELECT name, firstname, age from employees" + in + let print_employee row = +(* The `Ezsqlite.text`, `blob`, `int64`, `int`, `double` + functions can be used to read the values of individual columns. + + Note that this is not type-safe, since you need to provide the correct type + for the column here. *) + let name = Ezsqlite.text row 0 + and firstname = Ezsqlite.text row 1 + and age = Ezsqlite.int row 2 + in + Printf.printf "name=%s, firstname=%s, age=%d\n" + name firstname age + in + Ezsqlite.iter stmt print_employee