@odatnurd/d1-query
Version:
Simple D1 query wrapper
328 lines (247 loc) • 11.2 kB
Markdown
# Simple D1 Query
`d1-query` is a very simple set of wrapper functions that allow for working with
[Cloudflare D1](https://developers.cloudflare.com/d1/) queries in a
[Cloudflare Worker](https://developers.cloudflare.com/workers/) or in
[Cloudflare Pages](https://developers.cloudflare.com/pages/).
`D1` wraps [SQLite](https://sqlite.org/) and requires you to make queries via
`SQL` statements.
This library is a small convenience around this which in no way shields you
from having to write the actual SQL, but which makes it slightly more
expressive to create queries and batches of queries.
In addition, the library does logging to help trace the queries that are being
made, and how many rows were read and written as a part of it.
Also included is a [rollup](https://rollupjs.org/) plugin that allows for
importing SQL files directly, making it easier to bundle and use complex
queries without them having to be in code strings.
## Installation
Install `d1-query` via `npm`, `pnpm`, and so on, in the usual way.
```sh
npm install /d1-query
```
## Usage
`D1` requires you to provide it `SQL` statements to execute; this is generally
done via preparing a statement, optionally binding values to parameters in the
statement, and then executing it to fetch the results back.
The results of a D1 query take the form:
```json
{
"success": true,
"meta": {
"served_by": "miniflare.db",
"duration": 0,
"changes": 0,
"last_row_id": 1,
"changed_db": false,
"size_after": 12288,
"rows_read": 1,
"rows_written": 0
},
"results": [ { "id": 1, "name": "Bob" } ]
}
```
where the `meta` field provides information about the query, including how
many rows in the database had to be read or written to provide the data, with
the actual results (if any) showing up in the `results` key.
The routines in `d1-query` make it a bit easier and shorter to make requests,
particularly when reusing the same query multiple times via binds.
When queries are made, the return is stripped of the `meta`, providing you just
the value of the `results` (or in the case of `dbFetchOne`, the first argument
in the `results` array rather than the whole list).
As an addition convenience, returned results have any fields whose names start
with `is[A-Z]` converted into `bool` values, since `D1` accepts `true` or
`false` when doing inserts and maps them to `1` and `0` respectively, but does
not do the same mapping on the return.
## Methods
```js
export function dbPrepareStatements(db, ...sqlargs) {}
```
Given a database binding and an array of `sqlargs` (see below), return back a
prepared statement or statements ready to be executed on the given database.
The provided `sqlargs` are an open ended array that can consist of:
1. `D1PreparedStatement` instances from previously compiled SQL statements
2. strings that contain SQL queries to be compiled
3. arrays that contain parameter values to be bound to statements
When an array is seen, it is presumed to associate with the statement that
preceded it in the list, and will be used to bind arguments to the statement to
control the execution. It is possible for multiple entries in a row to be
arrays, allowing you to bind two versions of the same statement at once if
desired.
The return value is either a single prepared statement ready to execute, or an
array of prepared statements, depending on whether or not the input array
contains information for more than one statement or not.
---
```js
export async function dbRawQuery(db, statements, action) {}
```
Take as arguments the database in use, either a single prepared statement or an
array of prepared statements, and an indication of what is making the query,
and perform it.
A single statement is executed normally while an array is executed as a batch
of queries.
Logs will be generated outlining the results, and the results will be
returned back.
The returned results have the `D1` metadata stripped from them, so that they're
more useful to the caller.
---
```js
export async function dbFetch(db, action, ...sqlargs) {}
```
Execute a fetch operation on the provided database, using the data in `sqlargs`
to create the statement(s) to be executed, and return the result(s) of the
query after logging statistics such as the rows read and written, which will be
annotated with the action string provided to give context to the operation.
The provided `sqlargs` is a variable length list of arguments that consists of
strings to be compiled to SQL, previously compiled statements, and/or arrays of
values to bind to statements.
For the purposes of binding, arrays will bind to the most recently seen
statement, allowing you to compile one statement and bind it multiple times if
desired.
When more than one statement is provided, all statements will be executed as a
batch operation, which implicitly runs as a transaction.
The return value is the direct result of executing the query or queries given
in `sqlargs`; this is either a (potentially empty) array of result rows, or an
array of such arrays (if a batch). As with `dbRawQuery`, the `meta` is stripped
from the results, providing you just the actual query result.
---
```js
export async function dbFetchOne(db, action, ...sqlargs) {}
```
This executes as `dbFetch()` does, except that the return value is either the
first element of the result, or null if the result did not contain any rows.
When executed on a batch statement this will return the entire result set of
the first query in the batch, which may or may not be what you expect.
## Examples
The following examples assume that a `Cloudflare` worker has been configured
with a database binding such as the following in the `wrangler.toml` file:
```toml
[[d1_databases]]
binding = "DB"
database_name = "my-db-here"
database_id = "9c171c69-1142-1234-b2f4-e1e5d9c81928"
```
```js
// Prepare a single statement for later execution
const stmt1 = await dbPrepareStatements(ctx.env.DB,
'SELECT * FROM Users'
);
// Single statement, but searching for a specific user.
const stmt2 = await dbPrepareStatements(ctx.env.DB,
'SELECT * FROM Users WHERE userId = ? OR username = ?'
[1, 'bob']
);
// Reuse a previously compiled statement, but bind a new user.
const stmt3 = await dbPrepareStatements(ctx.env.DB,
stmt2,
[69, 'jim']
);
// Perform an insert; arrays of bind parameters always apply to the most
// recently seen statement in the array, so batches of queries can easily re-use
// the same compiled statement.
const inserts = await dbPrepareStatements(ctx.env.DB,
'INSERT INTO Users (userId, username) VALUES (?, ?)',
// Three arrays all bind to the same statement, creating a batch that
// executes the same statement three times, with different arguments each
// time.
[2, 'frank'],
[3, 'john'],
[4, 'alice']
);
```
Once you have prepared statements, you can execute them via `dbRawQuery`; this
takes the result of a previous call to `dbPrepareStatements` and executes them.
When given a single statement, the statement is executed and the result is
returned back as a (potentially empty) array of objects that represent the
matched rows.
When given an array of statements, all statements are executed as a `batch`; in
this mode, a `transaction` is used; so all statements either succeed or fail.
In this mode, the return value is an array of results; the resulting return will
contain one array for each of the statements.
The `action` argument to the method is a description of where or why the
query is being made; it is used in the resulting logged output to provide a
trace for why the query was made.
```js
// Execute the statement
let result = await dbRawQuery(ctx.env.DB, stmt1, 'fetch_users');
console.log(result);
// Produces:
// [0ms] fetch_users : OK : last_row_id=69, reads=2, writes=0, resultSize=2
// [ { userId: 1, username: 'bob' }, { userId: 69, username: 'jim' } ]
// This can also execute multiple statements as a batch.
result = await dbRawQuery(ctx.env.DB, inserts, 'insert_users');
console.log(result);
// Produces:
// [0ms] => insert_users : OK : last_row_id=2, reads=1, writes=1, resultSize=0
// [1ms] => insert_users : OK : last_row_id=3, reads=1, writes=1, resultSize=0
// [0ms] => insert_users : OK : last_row_id=4, reads=1, writes=1, resultSize=0
// [ [], [], [] ]
```
`dbFetch` is a convenience wrapper around the prior two functions, and is
useful for cases where you do not need to capture the statements that are
compiled and you just need to execute them.
The two examples above could also be expressed as the following; note that in
this form, the `action` comes before the SQL statements.
```js
let result = await dbFetch(ctx.env.DB, 'fetch_users', 'SELECT * FROM Users');
result = await dbFetch(ctx.env.DB, 'insert_users',
'INSERT INTO Users (userId, username) VALUES (?, ?)',
// Three arrays all bind to the same statement, creating a batch that
// executes the same statement three times, with different arguments each
// time.
[2, 'frank'],
[3, 'john'],
[4, 'alice']
);
```
`dbFetchOne` is a convenience wrapper around `dbFetch`; here if the result of
the query is no rows, `null` is returned; otherwise the return value is just
the first result, even if there are many results returned.
```js
let result = await dbFetch(ctx.env.DB, 'fetch_users', 'SELECT * FROM Users');
console.log(result);
// Produces
// [0ms] fetch_users : OK : last_row_id=69, reads=2, writes=0, resultSize=2
// [ { userId: 1, username: 'bob' }, { userId: 69, username: 'jim' } ]
result = await dbFetchOne(ctx.env.DB, 'fetch_users', 'SELECT * FROM Users');
console.log(result);
// Produces
// [0ms] fetch_users : OK : last_row_id=69, reads=2, writes=0, resultSize=2
// { userId: 1, username: 'bob' }
```
## Rollup Plugin
The package also includes a Rollup plugin that allows you to import a SQL file
and have it directly wrapped in a call to `dbPrepareStatements`, allowing for
easily handling more complex queries without them having to be inlined as
strings in the code.
To use the plugin, import it in your `rollup.config.js` file, and then include
it in the `plugins` list:
```js
// import the plugin
import d1sql from '/d1-query/rollup';
export default {
input: 'src/main.js',
output: {
file: 'dist/index.js',
format: 'es',
},
plugins: [
// Add the plugin to the plugin list.
d1sql(),
]
};
```
Once you've done this, you can import SQL files directly. The result of the
`import` is a function that takes as an argument a `D1` database handle, which
is used to prepare the statements. You can then execute the statements using
the normal API:
```js
// Import the desired API's, and also the SQL file to execute
import { dbFetch } from '/d1-query';
import query from './sample.sql';
// query is a function; calling it gives you the prepared SQL statements, ready
// for execution.
//
// The wrapper function caches the prepared statements after the first call you
// make, allowing for faster execution if you reuse the same statements again
// and again.
const result = await dbFetch(ctx.env.DB, 'test_query', query(ctx.env.DB));
```