Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,18 @@ for (const row of stmt) {
}
```

Use `iter()` to pass bind parameters to the same kind of iteration:

```ts
for (const row of stmt.iter("bar", "baz")) {
console.log(row);
}
```

Either way, the statement is reset as soon as the loop ends, including when you
`break` out of it or an exception leaves it, so a partly consumed iteration does
not hold a read transaction open on the connection.

## Transactions

To start a transaction, use the `transaction()` method. This method takes a
Expand Down
30 changes: 21 additions & 9 deletions src/statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,22 +831,34 @@ export class Statement<TStatement extends object = Record<string, any>> {
return readCstr(sqlite3_expanded_sql(this.#handle)!);
}

/** Iterate over resultant rows from query. */
/**
* Iterate over resultant rows from query.
*
* The statement is reset when the iterator finishes, whether it was
* exhausted, left early (`break`, `return`), or exited by an exception, so
* an abandoned iteration does not keep a read transaction open on the
* connection.
*/
*iter(...params: RestBindParameters): IterableIterator<any> {
this.#begin();
this.#bindAll(params);
const getRowObject = this.getRowObject();
const int64 = this.int64 ?? this.db.int64;
const parseJson = this.parseJson ?? this.db.parseJson;
let status = sqlite3_step(this.#handle);
while (status === SQLITE3_ROW) {
yield getRowObject(this.#handle, int64, parseJson);
status = sqlite3_step(this.#handle);
}
if (status !== SQLITE3_DONE) {
unwrap(status, this.db.unsafeHandle);
try {
let status = sqlite3_step(this.#handle);
while (status === SQLITE3_ROW) {
yield getRowObject(this.#handle, int64, parseJson);
status = sqlite3_step(this.#handle);
}
if (status !== SQLITE3_DONE) {
unwrap(status, this.db.unsafeHandle);
}
} finally {
// Ignore the result: after a failed step, reset returns that same error,
// which `unwrap` above has already thrown.
sqlite3_reset(this.#handle);
}
sqlite3_reset(this.#handle);
}

[Symbol.iterator](): IterableIterator<any> {
Expand Down
42 changes: 42 additions & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,48 @@ Deno.test("sqlite", async (t) => {
assertEquals(db.sql`select ${"1; DROP TABLE"}`, [{ "?": "1; DROP TABLE" }]);
});

await t.step("iterate (early exit resets the statement)", () => {
db.exec("create table iter_reset (id integer primary key)");
db.exec("insert into iter_reset (id) values (1), (2), (3)");
const stmt = db.prepare("select id from iter_reset");

// A statement left mid-iteration keeps a read cursor open on the table,
// which makes `drop table` fail with "database table is locked".
const dropTable = (): void => {
db.exec("drop table iter_reset");
db.exec("create table iter_reset (id integer primary key)");
db.exec("insert into iter_reset (id) values (1), (2), (3)");
};

for (const _ of stmt.iter()) break;
dropTable();

for (const _ of stmt) break;
dropTable();

assertThrows(
() => {
for (const _ of stmt.iter()) throw new Error("abandon");
},
Error,
"abandon",
);
dropTable();

// The open cursor also pins a WAL read snapshot, so once another
// connection commits, this connection cannot begin a write transaction.
for (const _ of stmt.iter()) break;
const other = new Database(DB_URL);
other.exec("insert into iter_reset (id) values (4)");
other.close();
db.exec("begin immediate");
db.exec("rollback");

assertEquals(stmt.all(), [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]);
stmt.finalize();
db.exec("drop table iter_reset");
});

await t.step("more than 32-bit int", () => {
const value = 978307200000;
db.exec(
Expand Down