Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cookbook database recipes #2376

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions data/cookbook/sqlite-create-insert-select/00-caqti-ppx-rapper.ml
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)

76 changes: 76 additions & 0 deletions data/cookbook/sqlite-create-insert-select/01-ezsqlite.ml
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
Loading