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 5 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
174 changes: 174 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,174 @@
---
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. The declaration of its queries is quite sophisticated:
`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_person_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_person_table =
[%rapper
execute {sql| CREATE TABLE person
(name VARCHAR,
firstname VARCHAR,
age INTEGER)
|sql}
]

type person =
{ name:string; firstname:string; age:int }
let people = [
{name = "Dupont"; firstname = "Jacques"; age = 36};
{name = "Legendre"; firstname = "Patrick"; age = 42}
]

(* For the SQL `INSERT` query, `ppx_rapper` generates a function `insert_person (p: person) 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]{..}` notation specifies
which conversions to perform on the input values. *)
let insert_person =
[%rapper
execute
{sql| INSERT INTO person 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]{..}` notation specifies
which conversions to perform on the output values.
*)
let get_all_person =
[%rapper
get_many
{sql|SELECT
@string{name},
@string{firstname},
@int{age}
FROM person
|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_person_by_name =
[%rapper
get_opt
{sql|SELECT
@string{name},
@string{firstname},
@int{age}
FROM personal
WHERE name=%string{name}
|sql}
record_out
]


(* All query functions generated by `ppx_rapper` take an argument and a `connection` parameter.
The function `insert_person` must be called with
`record_of_person` and `connection`. If multiple records from
a list should be inserted, `List.map` creates a list
of functions. Each of these functions will execute its
associated query when called. The function `iter_queries` runs
the queries in sequence. *)
let execute_queries connection =
let*? () = create_person_table () connection in
let*? () =
iter_queries (List.map insert_person people) connection
in
let*? people = get_all_person () connection in
people |> List.iter (fun person ->
Printf.printf "name=%s, firstname=%s, age=%d\n"
person.name person.firstname person.age);
let*? person =
get_person_by_name ~name:"Dupont" connection
in
match person with
| Some person' ->
Printf.printf "found:name=%s, firstname=%s, age=%d\n"
person'.name person'.firstname person'.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)

49 changes: 49 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,49 @@
---
packages:
- name: "ezsqlite"
tested_version: "0.4.2"
used_libraries:
- ezsqlite
discussion: |
- **Understanding `Ezsqlite`:** The `Ezsqlite` libraies proposes a set of function which permits the usage of a SQLite3 database. They are described in the [`Ezsqlite` library documentation](https://github.com/zshipko/ocaml-ezsqlite/blob/master/lib/ezsqlite.mli). It uses exception for error handling.
- **Alternative Libraries:** `sqlite3` is an other SQLite library. The `gensqlite` is a preprocessor for `sqlite3`. `sqlexpr` is another SQLite library which comes with a PPX preprocessor. `caqti` is a portable interface which supports SQLite3, MariaDB and PostgreSQL. It may be used with the `ppx_rapper` preprocessor.
---

(* Use the `ezsqlite` library, which permits the use of a SQLite3 database. Before any use, the database much be opened. The `load` creates the database is it doesn't exist: *)
let db = Ezsqlite.load "personal.sqlite"

(* Table creation. The `run_ign` function is used when no values are returned by the query. *)
let () =
Ezsqlite.run_ign db
"CREATE TABLE personal (name VARCHAR, firstname VARCHAR, age INTEGER)"
()

(* Row insertions. First, the statement is prepared (parsed, compiled), then each ":id" is bound to actual values. Then the query is executed. It is recommended to have constant query strings and use bindings to deal with variable values, especially with values from an unstrusted source. *)
type person = { name:string; firstname:string; age:int }

let persons = [ {name="Dupont"; firstname="Jacques"; age=36};
{name="Legendre"; firstname="Patrick"; age=42} ]

let () =
let stmt = Ezsqlite.prepare db
"INSERT into personal VALUES (:name, :firstname, :age)" in
persons
|> List.iter (fun r ->
Ezsqlite.clear stmt;
Ezsqlite.bind_dict stmt
[":name", Ezsqlite.Value.Text r.name;
":firstname", Ezsqlite.Value.Text r.firstname;
":age", Ezsqlite.Value.Integer (Int64.of_int r.age)];
Ezsqlite.exec stmt)

(* Selection of rows. The `iter` function can execute a query while executing a given function for each row. The `text`, `blob`, `int64`, `int`, `double` functions can be used to get the values returned by the query. `column` and `Value.is_null` functions can be used if we have to check the nullity (NULL SQL value) of some values. *)
let () =
let stmt = Ezsqlite.prepare db
"SELECT name, firstname, age from personal" in
Ezsqlite.iter stmt
(fun stmt ->
let name=Ezsqlite.text stmt 0
and firstname=Ezsqlite.text stmt 1
and age=Ezsqlite.int stmt 2
in
Printf.printf "name=%s, firstname=%s, age=%d\n" name firstname age)
Loading