From 035ede6fc1c84bc0086b4e20045929e2ba698d44 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 28 Apr 2026 12:04:42 +0300 Subject: [PATCH] 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. --- docs/api.md | 48 +++++++++++++ index.d.ts | 2 +- integration-tests/tests/async.test.js | 97 +++++++++++++++++++++++++++ promise.js | 49 +++++++++++++- 4 files changed, 194 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index 026e0f3..eb1e55d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -36,6 +36,54 @@ Prepares a SQL statement for execution. The function returns a `Statement` object. +### run(sql[, ...bindParameters][, queryOptions]) ⇒ object + +Convenience wrapper that prepares `sql` and executes `Statement.run`. Returns the same info object as `Statement.run` (`changes` and `lastInsertRowid`). + +| Param | Type | Description | +| -------------- | ------------------- | -------------------------------------------------------------------- | +| sql | string | The SQL statement string. | +| bindParameters | any | Optional positional or named bind parameters. | +| queryOptions | object | Optional per-query overrides (for example, `{ queryTimeout: 100 }`). | + +**Note:** This is an extension in libSQL and not available in `better-sqlite3`. + +### get(sql[, ...bindParameters][, queryOptions]) ⇒ row + +Convenience wrapper that prepares `sql` and executes `Statement.get`. Returns the first row, or `undefined` if no row matched. + +| Param | Type | Description | +| -------------- | ------------------- | -------------------------------------------------------------------- | +| sql | string | The SQL statement string. | +| bindParameters | any | Optional positional or named bind parameters. | +| queryOptions | object | Optional per-query overrides (for example, `{ queryTimeout: 100 }`). | + +**Note:** This is an extension in libSQL and not available in `better-sqlite3`. + +### all(sql[, ...bindParameters][, queryOptions]) ⇒ array of rows + +Convenience wrapper that prepares `sql` and executes `Statement.all`. Returns all matching rows as an array. + +| Param | Type | Description | +| -------------- | ------------------- | -------------------------------------------------------------------- | +| sql | string | The SQL statement string. | +| bindParameters | any | Optional positional or named bind parameters. | +| queryOptions | object | Optional per-query overrides (for example, `{ queryTimeout: 100 }`). | + +**Note:** This is an extension in libSQL and not available in `better-sqlite3`. + +### iterate(sql[, ...bindParameters][, queryOptions]) ⇒ iterator + +Convenience wrapper that prepares `sql` and executes `Statement.iterate`. Returns an async iterator over the resulting rows. + +| Param | Type | Description | +| -------------- | ------------------- | -------------------------------------------------------------------- | +| sql | string | The SQL statement string. | +| bindParameters | any | Optional positional or named bind parameters. | +| queryOptions | object | Optional per-query overrides (for example, `{ queryTimeout: 100 }`). | + +**Note:** This is an extension in libSQL and not available in `better-sqlite3`. + ### transaction(function) ⇒ function Returns a function that runs the given function in a transaction. diff --git a/index.d.ts b/index.d.ts index 9cdec0a..91e6140 100644 --- a/index.d.ts +++ b/index.d.ts @@ -199,8 +199,8 @@ export declare class Statement { } /** A raw iterator over rows. The JavaScript layer wraps this in a iterable. */ export declare class RowsIterator { - close(): void next(): Promise + close(): void } export declare class Record { get value(): unknown diff --git a/integration-tests/tests/async.test.js b/integration-tests/tests/async.test.js index 28b4381..38f24d2 100644 --- a/integration-tests/tests/async.test.js +++ b/integration-tests/tests/async.test.js @@ -605,6 +605,103 @@ test.serial("Statement.reader [DELETE RETURNING is true]", async (t) => { t.is(stmt.reader, true); }); +test.serial("Database.run() [positional]", async (t) => { + const db = t.context.db; + + const info = await db.run( + "INSERT INTO users(name, email) VALUES (?, ?)", + "Carol", + "carol@example.net" + ); + t.is(info.changes, 1); + t.is(info.lastInsertRowid, 3); + + const row = await db.get("SELECT name, email FROM users WHERE id = ?", 3); + t.is(row.name, "Carol"); + t.is(row.email, "carol@example.net"); +}); + +test.serial("Database.run() [named]", async (t) => { + const db = t.context.db; + + const info = await db.run( + "INSERT INTO users(name, email) VALUES (:name, :email)", + { name: "Carol", email: "carol@example.net" } + ); + t.is(info.changes, 1); + t.is(info.lastInsertRowid, 3); +}); + +test.serial("Database.get() returns no rows", async (t) => { + const db = t.context.db; + t.is(await db.get("SELECT * FROM users WHERE id = ?", 0), undefined); +}); + +test.serial("Database.get() [positional]", async (t) => { + const db = t.context.db; + t.is((await db.get("SELECT * FROM users WHERE id = ?", 1)).name, "Alice"); + t.is((await db.get("SELECT * FROM users WHERE id = ?", 2)).name, "Bob"); +}); + +test.serial("Database.get() [named]", async (t) => { + const db = t.context.db; + t.is( + (await db.get("SELECT * FROM users WHERE id = :id", { id: 1 })).name, + "Alice" + ); +}); + +test.serial("Database.all()", async (t) => { + const db = t.context.db; + const expected = [ + { id: 1, name: "Alice", email: "alice@example.org" }, + { id: 2, name: "Bob", email: "bob@example.com" }, + ]; + t.deepEqual(await db.all("SELECT * FROM users"), expected); +}); + +test.serial("Database.all() [positional]", async (t) => { + const db = t.context.db; + const expected = [{ id: 1, name: "Alice", email: "alice@example.org" }]; + t.deepEqual(await db.all("SELECT * FROM users WHERE id = ?", 1), expected); +}); + +test.serial("Database.iterate()", async (t) => { + const db = t.context.db; + const expected = [1, 2]; + let idx = 0; + for await (const row of await db.iterate("SELECT * FROM users")) { + t.is(row.id, expected[idx++]); + } + t.is(idx, 2); +}); + +test.serial("Database.iterate() [positional]", async (t) => { + const db = t.context.db; + let count = 0; + for await (const row of await db.iterate( + "SELECT * FROM users WHERE id = ?", + 2 + )) { + t.is(row.name, "Bob"); + count++; + } + t.is(count, 1); +}); + +test.serial("Database.run() forwards queryOptions", async (t) => { + const db = t.context.db; + await t.throwsAsync( + async () => { + await db.run( + "WITH RECURSIVE infinite(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM infinite) SELECT count(*) FROM infinite", + { queryTimeout: 50 } + ); + }, + { instanceOf: t.context.errorType, code: "SQLITE_INTERRUPT" } + ); +}); + const connect = async (path_opt, options = {}) => { const path = path_opt ?? "hello.db"; const provider = process.env.PROVIDER; diff --git a/promise.js b/promise.js index 95ca75b..f020de7 100644 --- a/promise.js +++ b/promise.js @@ -52,7 +52,10 @@ function splitBindParameters(bindParameters) { if (bindParameters.length === 0) { return { params: undefined, queryOptions: undefined }; } - if (bindParameters.length > 1 && isQueryOptions(bindParameters[bindParameters.length - 1])) { + if (isQueryOptions(bindParameters[bindParameters.length - 1])) { + if (bindParameters.length === 1) { + return { params: undefined, queryOptions: bindParameters[0] }; + } return { params: bindParameters.length === 2 ? bindParameters[0] : bindParameters.slice(0, -1), queryOptions: bindParameters[bindParameters.length - 1], @@ -169,6 +172,50 @@ class Database { return properties.default.value; } + /** + * Prepares the SQL and executes it as `Statement.run`, returning the run info. + * + * @param {string} sql - The SQL statement string. + * @param {...any} bindParameters - Bind parameters, optionally followed by a query options object. + */ + async run(sql, ...bindParameters) { + const stmt = await this.prepare(sql); + return await stmt.run(...bindParameters); + } + + /** + * Prepares the SQL and executes it as `Statement.get`, returning the first row. + * + * @param {string} sql - The SQL statement string. + * @param {...any} bindParameters - Bind parameters, optionally followed by a query options object. + */ + async get(sql, ...bindParameters) { + const stmt = await this.prepare(sql); + return await stmt.get(...bindParameters); + } + + /** + * Prepares the SQL and executes it as `Statement.all`, returning all rows. + * + * @param {string} sql - The SQL statement string. + * @param {...any} bindParameters - Bind parameters, optionally followed by a query options object. + */ + async all(sql, ...bindParameters) { + const stmt = await this.prepare(sql); + return await stmt.all(...bindParameters); + } + + /** + * Prepares the SQL and executes it as `Statement.iterate`, returning an async iterator over rows. + * + * @param {string} sql - The SQL statement string. + * @param {...any} bindParameters - Bind parameters, optionally followed by a query options object. + */ + async iterate(sql, ...bindParameters) { + const stmt = await this.prepare(sql); + return await stmt.iterate(...bindParameters); + } + /** * Execute a pragma statement * @param {string} source - The pragma statement to execute, without the `PRAGMA` prefix.