Skip to content

Commit 035ede6

Browse files
committed
Add bind parameter support to Database API
Currently, you need to first prepare a statement to use bind parameters. Add convience wrappers to Database so that callers are not required to do that. This allows optimization too for the Turso serverless driver too, because we no longer need two round trips per query, one for prepare and anoher for query.
1 parent 2f810cd commit 035ede6

4 files changed

Lines changed: 194 additions & 2 deletions

File tree

docs/api.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,54 @@ Prepares a SQL statement for execution.
3636

3737
The function returns a `Statement` object.
3838

39+
### run(sql[, ...bindParameters][, queryOptions]) ⇒ object
40+
41+
Convenience wrapper that prepares `sql` and executes `Statement.run`. Returns the same info object as `Statement.run` (`changes` and `lastInsertRowid`).
42+
43+
| Param | Type | Description |
44+
| -------------- | ------------------- | -------------------------------------------------------------------- |
45+
| sql | <code>string</code> | The SQL statement string. |
46+
| bindParameters | <code>any</code> | Optional positional or named bind parameters. |
47+
| queryOptions | <code>object</code> | Optional per-query overrides (for example, `{ queryTimeout: 100 }`). |
48+
49+
**Note:** This is an extension in libSQL and not available in `better-sqlite3`.
50+
51+
### get(sql[, ...bindParameters][, queryOptions]) ⇒ row
52+
53+
Convenience wrapper that prepares `sql` and executes `Statement.get`. Returns the first row, or `undefined` if no row matched.
54+
55+
| Param | Type | Description |
56+
| -------------- | ------------------- | -------------------------------------------------------------------- |
57+
| sql | <code>string</code> | The SQL statement string. |
58+
| bindParameters | <code>any</code> | Optional positional or named bind parameters. |
59+
| queryOptions | <code>object</code> | Optional per-query overrides (for example, `{ queryTimeout: 100 }`). |
60+
61+
**Note:** This is an extension in libSQL and not available in `better-sqlite3`.
62+
63+
### all(sql[, ...bindParameters][, queryOptions]) ⇒ array of rows
64+
65+
Convenience wrapper that prepares `sql` and executes `Statement.all`. Returns all matching rows as an array.
66+
67+
| Param | Type | Description |
68+
| -------------- | ------------------- | -------------------------------------------------------------------- |
69+
| sql | <code>string</code> | The SQL statement string. |
70+
| bindParameters | <code>any</code> | Optional positional or named bind parameters. |
71+
| queryOptions | <code>object</code> | Optional per-query overrides (for example, `{ queryTimeout: 100 }`). |
72+
73+
**Note:** This is an extension in libSQL and not available in `better-sqlite3`.
74+
75+
### iterate(sql[, ...bindParameters][, queryOptions]) ⇒ iterator
76+
77+
Convenience wrapper that prepares `sql` and executes `Statement.iterate`. Returns an async iterator over the resulting rows.
78+
79+
| Param | Type | Description |
80+
| -------------- | ------------------- | -------------------------------------------------------------------- |
81+
| sql | <code>string</code> | The SQL statement string. |
82+
| bindParameters | <code>any</code> | Optional positional or named bind parameters. |
83+
| queryOptions | <code>object</code> | Optional per-query overrides (for example, `{ queryTimeout: 100 }`). |
84+
85+
**Note:** This is an extension in libSQL and not available in `better-sqlite3`.
86+
3987
### transaction(function) ⇒ function
4088

4189
Returns a function that runs the given function in a transaction.

index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@ export declare class Statement {
199199
}
200200
/** A raw iterator over rows. The JavaScript layer wraps this in a iterable. */
201201
export declare class RowsIterator {
202-
close(): void
203202
next(): Promise<Record>
203+
close(): void
204204
}
205205
export declare class Record {
206206
get value(): unknown

integration-tests/tests/async.test.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,103 @@ test.serial("Statement.reader [DELETE RETURNING is true]", async (t) => {
605605
t.is(stmt.reader, true);
606606
});
607607

608+
test.serial("Database.run() [positional]", async (t) => {
609+
const db = t.context.db;
610+
611+
const info = await db.run(
612+
"INSERT INTO users(name, email) VALUES (?, ?)",
613+
"Carol",
614+
"carol@example.net"
615+
);
616+
t.is(info.changes, 1);
617+
t.is(info.lastInsertRowid, 3);
618+
619+
const row = await db.get("SELECT name, email FROM users WHERE id = ?", 3);
620+
t.is(row.name, "Carol");
621+
t.is(row.email, "carol@example.net");
622+
});
623+
624+
test.serial("Database.run() [named]", async (t) => {
625+
const db = t.context.db;
626+
627+
const info = await db.run(
628+
"INSERT INTO users(name, email) VALUES (:name, :email)",
629+
{ name: "Carol", email: "carol@example.net" }
630+
);
631+
t.is(info.changes, 1);
632+
t.is(info.lastInsertRowid, 3);
633+
});
634+
635+
test.serial("Database.get() returns no rows", async (t) => {
636+
const db = t.context.db;
637+
t.is(await db.get("SELECT * FROM users WHERE id = ?", 0), undefined);
638+
});
639+
640+
test.serial("Database.get() [positional]", async (t) => {
641+
const db = t.context.db;
642+
t.is((await db.get("SELECT * FROM users WHERE id = ?", 1)).name, "Alice");
643+
t.is((await db.get("SELECT * FROM users WHERE id = ?", 2)).name, "Bob");
644+
});
645+
646+
test.serial("Database.get() [named]", async (t) => {
647+
const db = t.context.db;
648+
t.is(
649+
(await db.get("SELECT * FROM users WHERE id = :id", { id: 1 })).name,
650+
"Alice"
651+
);
652+
});
653+
654+
test.serial("Database.all()", async (t) => {
655+
const db = t.context.db;
656+
const expected = [
657+
{ id: 1, name: "Alice", email: "alice@example.org" },
658+
{ id: 2, name: "Bob", email: "bob@example.com" },
659+
];
660+
t.deepEqual(await db.all("SELECT * FROM users"), expected);
661+
});
662+
663+
test.serial("Database.all() [positional]", async (t) => {
664+
const db = t.context.db;
665+
const expected = [{ id: 1, name: "Alice", email: "alice@example.org" }];
666+
t.deepEqual(await db.all("SELECT * FROM users WHERE id = ?", 1), expected);
667+
});
668+
669+
test.serial("Database.iterate()", async (t) => {
670+
const db = t.context.db;
671+
const expected = [1, 2];
672+
let idx = 0;
673+
for await (const row of await db.iterate("SELECT * FROM users")) {
674+
t.is(row.id, expected[idx++]);
675+
}
676+
t.is(idx, 2);
677+
});
678+
679+
test.serial("Database.iterate() [positional]", async (t) => {
680+
const db = t.context.db;
681+
let count = 0;
682+
for await (const row of await db.iterate(
683+
"SELECT * FROM users WHERE id = ?",
684+
2
685+
)) {
686+
t.is(row.name, "Bob");
687+
count++;
688+
}
689+
t.is(count, 1);
690+
});
691+
692+
test.serial("Database.run() forwards queryOptions", async (t) => {
693+
const db = t.context.db;
694+
await t.throwsAsync(
695+
async () => {
696+
await db.run(
697+
"WITH RECURSIVE infinite(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM infinite) SELECT count(*) FROM infinite",
698+
{ queryTimeout: 50 }
699+
);
700+
},
701+
{ instanceOf: t.context.errorType, code: "SQLITE_INTERRUPT" }
702+
);
703+
});
704+
608705
const connect = async (path_opt, options = {}) => {
609706
const path = path_opt ?? "hello.db";
610707
const provider = process.env.PROVIDER;

promise.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ function splitBindParameters(bindParameters) {
5252
if (bindParameters.length === 0) {
5353
return { params: undefined, queryOptions: undefined };
5454
}
55-
if (bindParameters.length > 1 && isQueryOptions(bindParameters[bindParameters.length - 1])) {
55+
if (isQueryOptions(bindParameters[bindParameters.length - 1])) {
56+
if (bindParameters.length === 1) {
57+
return { params: undefined, queryOptions: bindParameters[0] };
58+
}
5659
return {
5760
params: bindParameters.length === 2 ? bindParameters[0] : bindParameters.slice(0, -1),
5861
queryOptions: bindParameters[bindParameters.length - 1],
@@ -169,6 +172,50 @@ class Database {
169172
return properties.default.value;
170173
}
171174

175+
/**
176+
* Prepares the SQL and executes it as `Statement.run`, returning the run info.
177+
*
178+
* @param {string} sql - The SQL statement string.
179+
* @param {...any} bindParameters - Bind parameters, optionally followed by a query options object.
180+
*/
181+
async run(sql, ...bindParameters) {
182+
const stmt = await this.prepare(sql);
183+
return await stmt.run(...bindParameters);
184+
}
185+
186+
/**
187+
* Prepares the SQL and executes it as `Statement.get`, returning the first row.
188+
*
189+
* @param {string} sql - The SQL statement string.
190+
* @param {...any} bindParameters - Bind parameters, optionally followed by a query options object.
191+
*/
192+
async get(sql, ...bindParameters) {
193+
const stmt = await this.prepare(sql);
194+
return await stmt.get(...bindParameters);
195+
}
196+
197+
/**
198+
* Prepares the SQL and executes it as `Statement.all`, returning all rows.
199+
*
200+
* @param {string} sql - The SQL statement string.
201+
* @param {...any} bindParameters - Bind parameters, optionally followed by a query options object.
202+
*/
203+
async all(sql, ...bindParameters) {
204+
const stmt = await this.prepare(sql);
205+
return await stmt.all(...bindParameters);
206+
}
207+
208+
/**
209+
* Prepares the SQL and executes it as `Statement.iterate`, returning an async iterator over rows.
210+
*
211+
* @param {string} sql - The SQL statement string.
212+
* @param {...any} bindParameters - Bind parameters, optionally followed by a query options object.
213+
*/
214+
async iterate(sql, ...bindParameters) {
215+
const stmt = await this.prepare(sql);
216+
return await stmt.iterate(...bindParameters);
217+
}
218+
172219
/**
173220
* Execute a pragma statement
174221
* @param {string} source - The pragma statement to execute, without the `PRAGMA` prefix.

0 commit comments

Comments
 (0)