requery
is a library for interacting with a SQL database from a ReasonML/Ocaml application. It includes a generic SQL AST and combinators for constructing queries and parsing the results of these queries into domain objects. It is inspired by knex.js
, but leveraging the type system of ML for correctness and expressiveness.
requery
is currently dependent on being built with bucklescript
and the javascript ecosystem. Future work might enable it to be used in other ecosystems as well.
let (then_, resolve) = Js.Promise.(then_, resolve);
let client = RequerySqlite.Sqlite3.(makeClient(Memory));
let authors = QueryBuilder.tname("authors");
RowEncode.(
[("Stephen", "King"), ("Jane", "Austen"), ("Kurt", "Vonnegut")]
|> insertMany(columns2("first", string, "last", string))
|> into(authors)
)
|> Client.insert(client)
|> then_(_ =>
QueryBuilder.(select([e(col("first")), e(col("last"))] |> from(table(authors))))
|> Client.select(
client,
RowDecode.(decodeEach(columns2("first", string, "last", string))),
)
)
|> then_(authors => authors |> Js.log |> resolve);
The components of requery
are designed to be modular and each can be used in whatever capacity you need. You might use it to:
RowDecode
library to unpack the results of queries you've written by handNote that while an ORM could be written using requery
to structure queries, requery
itself is not an ORM. It does not enforce or encourage any particular framework for how you structure your tables or do migrations; instead it (hopefully) provides you with the ability to build SQL however you'd like.
Sql
: contains an abstact syntax tree for SQL. The AST is polymorphic to support DB-specific syntax. The types here are generally not used directly; instead use the functions in QueryBuilder
.QueryBuilder
: functions for building SQL queries in a more ergonomic way than directly constructing an AST (although you can if you want). See the interface file QueryBuilder.rei
for documentation on the various builder functions.RenderQuery
: Code to render the AST objects into actual SQL strings. You can use this library directly if you need access to the SQL, but if you're using the Client
this will probably be abstracted away.RowEncode
: functions to serialize domain objects into "rows", that is, the data that goes into an INSERT INTO
query.RowDecode
: functions to deserialize information returned by a query (e.g. a SELECT
or an INSERT
which returns data) into domain objects.Client
: an abstraction of the actual database object. This allows you to interact with your database using the requery
abstractions.PostgresSyntax
: type-safe AST for PostgresQL. Very much a WIP.PostgresClient
: functionality to connect to a postgres database.Let's say you have a Postgres database of books and authors, with the following tables and data. Note that we can use requery
to create the table and insert rows, but since we're focusing on SELECT queries, we'll save that for later:
CREATE TABLE authors (id SERIAL PRIMARY KEY, first_name TEXT, last_name TEXT);
CREATE TABLE books (
id SERIAL PRIMARY KEY,
author_id INT NOT NULL,
title TEXT NOT NULL,
FOREIGN KEY (author_id) REFERENCES authors(id)
);
INSERT INTO authors (first_name, last_name) VALUES ('Stephen', 'King');
INSERT INTO books (author_id, title) VALUES (1, 'The Shining'), (1, 'Carrie');
Start off by adding @adnelson/requery
as a dependency. Don't forget to update your bsconfig.json
as well by putting "@adnelson/requery"
under bs-dependencies
.
One thing you might want to do is find all of the books that an author wrote. Here's an example of how that might look:
let booksByAuthor = (authorId: int): select => Requery.QueryBuilder.(
select([
e(tcol("authors", "first_name") ++ string(" ") ++ tcol("authors", "last_name"), ~a="name"),
e(tcol("books", "title")),
])
|> from(
tableNamed("authors")
|> innerJoin(tableNamed("books"),
tcol("authors", "id") == tcol("books", "author_id"))
)
|> where(tcol("authors", "id") == int(authorId))
);
Js.log(Requery.Postgres.Render.select(booksByAuthor(1)));
Output:
SELECT "authors"."first_name" || ' ' || "authors"."last_name" AS name, "books"."title"
FROM authors INNER JOIN books ON "authors"."id" = "books"."author_id"
WHERE "authors"."id" = 1
If I pipe this into psql
:
⇒ node example/Books.bs.js | psql requery-example
name | title
--------------+-------------
Stephen King | The Shining
Stephen King | Carrie
(2 rows)
Now of course, for a query like this the Reason code is considerably more verbose than the query which is generated at the end. But the advantage is that this query can be reused! Maybe all you need to know is the number of books the author wrote. We can leverage the query we wrote before:
let bookCountByAuthor = (authorId: int): select => Requery.QueryBuilder.(
select([e(col("name")), e(count(all))])
|> from(booksByAuthor(authorId) |> selectAs("t"))
|> groupBy1(column("name"))
);
Js.log(Requery.Postgres.Render.select(bookCountByAuthor(1)));
Output:
SELECT "name", COUNT(*) FROM (
SELECT "authors"."first_name" || ' ' || "authors"."last_name" AS name, "books"."title"
FROM authors INNER JOIN books ON "authors"."id" = "books"."author_id"
WHERE "authors"."id" = 1
) AS t
GROUP BY "name"
Result:
⇒ node example/Books.bs.js | psql requery-example
name | count
--------------+-------
Stephen King | 2
(1 row)
The QueryBuilder
library will ensure that whatever logic you follow to construct a query, the end result will be syntactically valid SQL. Of course, it does not ensure that the query will return the data you expect, or any data at all -- that's still up to you.
For a more complete example, which includes table creation, insertion and selection, see examples/Books.re
, examples/SqliteBooks.re
and examples.PostgresBooks.re
.
At present, the following query types have been implemented, with the following components. This list will be updated over time.
&&
, ||
, LIKE
IS NOT NULL
, etcCOUNT(*)
FROM
clauses
SELECT * FROM (SELECT ...) AS t
)JOIN
clausesINNER JOIN
, LEFT JOIN
, RIGHT JOIN
, FULL JOIN
and CROSS JOIN
GROUP BY
one or more columnsORDER BY
one or more columns (with optional DESC
/ASC
)LIMIT
clausesWHERE
clausesVALUES
, organized as one or more tuples of (column, expression)
SELECT
queryIF NOT EXISTS
PRIMARY KEY
, UNIQUE
, NOT NULL
, CHECK
and DEFAULT
constraintsPRIMARY KEY
, FOREIGN KEY
, UNIQUE
, and CHECK
constraintsSELECT
queryIF NOT EXISTS
PostgresQL. At one point SQLite had support and that might return, but I don't use it, the package doesn't build out of the box on nixos and I just haven't figured out how to get around it yet. Of course anyone can write their own library around it.
NOTE: Requery's API is unstable and subject to change without notice. This doesn't mean that the code is expected to be of poor quality, just that there may be any number of breaking changes until a hypothetical 1.0 release.
There's plenty left to do, and much will likely change, but at this point the library is at least worthy of playing around with for personal projects. The QueryBuilder
library can be used to build useful queries of pretty sophiticated complexity, the RenderQuery
library can render these into valid SQL, and functions exist for basic database interaction including object serialization/deserialization.
Planned upcoming work includes:
DELETE FROM
and DROP TABLE
queries.WITH
, UNION
and UNION ALL
syntax for SELECT
queries.Contributions and issue reports are very much welcome!