-
Notifications
You must be signed in to change notification settings - Fork 326
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(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 <[email protected]> Co-authored-by: Christine Rose <[email protected]> Co-authored-by: sabine <[email protected]>
- Loading branch information
1 parent
3f8f45a
commit b91eed1
Showing
2 changed files
with
260 additions
and
0 deletions.
There are no files selected for viewing
184 changes: 184 additions & 0 deletions
184
data/cookbook/sqlite-create-insert-select/00-caqti-ppx-rapper.ml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |