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.