Skip to content

Commit e4a845e

Browse files
committed
Add statement lifecycle management and explicit close support
Track prepared statements via WeakRefs so Database.close() automatically closes all associated statements. Add explicit Statement.close() for manual resource management and Symbol.dispose support for the `using` syntax. Use-after-close now throws a clear TypeError instead of potentially undefined behavior.
1 parent 0ac7e3a commit e4a845e

7 files changed

Lines changed: 275 additions & 61 deletions

File tree

compat.js

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ function splitBindParameters(bindParameters) {
4848
return { params: bindParameters.length === 1 ? bindParameters[0] : bindParameters, queryOptions: undefined };
4949
}
5050

51+
const symbolDispose = typeof Symbol.dispose === "symbol" ? Symbol.dispose : null;
52+
const hasWeakRef = typeof WeakRef === "function";
53+
5154
/**
5255
* Database represents a connection that can prepare and execute SQL statements.
5356
*/
@@ -61,6 +64,8 @@ class Database {
6164
constructor(path, opts) {
6265
this.db = new NativeDb(path, opts);
6366
this.memory = this.db.memory
67+
this._closed = false;
68+
this._statements = hasWeakRef ? new Set() : null;
6469
const db = this.db;
6570
Object.defineProperties(this, {
6671
inTransaction: {
@@ -95,7 +100,13 @@ class Database {
95100
prepare(sql) {
96101
try {
97102
const stmt = databasePrepareSync(this.db, sql);
98-
return new Statement(stmt);
103+
const wrappedStmt = new Statement(stmt, this);
104+
if (this._statements != null) {
105+
const statementRef = new WeakRef(wrappedStmt);
106+
wrappedStmt._statementRef = statementRef;
107+
this._statements.add(statementRef);
108+
}
109+
return wrappedStmt;
99110
} catch (err) {
100111
throw convertError(err);
101112
}
@@ -215,6 +226,21 @@ class Database {
215226
* Closes the database connection.
216227
*/
217228
close() {
229+
if (this._closed) {
230+
return;
231+
}
232+
this._closed = true;
233+
if (this._statements != null) {
234+
for (const statementRef of Array.from(this._statements)) {
235+
const statement = statementRef.deref();
236+
if (statement == null) {
237+
this._statements.delete(statementRef);
238+
continue;
239+
}
240+
statement.close();
241+
}
242+
this._statements.clear();
243+
}
218244
this.db.close();
219245
}
220246

@@ -240,8 +266,36 @@ class Database {
240266
* Statement represents a prepared SQL statement that can be executed.
241267
*/
242268
class Statement {
243-
constructor(stmt) {
269+
constructor(stmt, database) {
244270
this.stmt = stmt;
271+
this.database = database;
272+
this._statementRef = null;
273+
this._closed = false;
274+
}
275+
276+
close() {
277+
if (this._closed) {
278+
return this;
279+
}
280+
this._closed = true;
281+
if (this.database != null && this.database._statements != null && this._statementRef != null) {
282+
this.database._statements.delete(this._statementRef);
283+
}
284+
if (this.database != null) {
285+
this.database = null;
286+
}
287+
if (this.stmt != null) {
288+
this.stmt.close();
289+
this.stmt = null;
290+
}
291+
return this;
292+
}
293+
294+
_getNativeStatement() {
295+
if (this._closed || this.stmt == null) {
296+
throw new TypeError("The database connection is not open");
297+
}
298+
return this.stmt;
245299
}
246300

247301
/**
@@ -250,7 +304,7 @@ class Statement {
250304
* @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled.
251305
*/
252306
raw(raw) {
253-
this.stmt.raw(raw);
307+
this._getNativeStatement().raw(raw);
254308
return this;
255309
}
256310

@@ -260,7 +314,7 @@ class Statement {
260314
* @param pluckMode Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled.
261315
*/
262316
pluck(pluckMode) {
263-
this.stmt.pluck(pluckMode);
317+
this._getNativeStatement().pluck(pluckMode);
264318
return this;
265319
}
266320

@@ -270,12 +324,12 @@ class Statement {
270324
* @param timing Enable or disable query timing. If you don't pass the parameter, query timing is enabled.
271325
*/
272326
timing(timingMode) {
273-
this.stmt.timing(timingMode);
327+
this._getNativeStatement().timing(timingMode);
274328
return this;
275329
}
276330

277331
get reader() {
278-
return this.stmt.columns().length > 0;
332+
return this._getNativeStatement().columns().length > 0;
279333
}
280334

281335
/**
@@ -284,7 +338,7 @@ class Statement {
284338
run(...bindParameters) {
285339
try {
286340
const { params, queryOptions } = splitBindParameters(bindParameters);
287-
return statementRunSync(this.stmt, params, queryOptions);
341+
return statementRunSync(this._getNativeStatement(), params, queryOptions);
288342
} catch (err) {
289343
throw convertError(err);
290344
}
@@ -298,7 +352,7 @@ class Statement {
298352
get(...bindParameters) {
299353
try {
300354
const { params, queryOptions } = splitBindParameters(bindParameters);
301-
return statementGetSync(this.stmt, params, queryOptions);
355+
return statementGetSync(this._getNativeStatement(), params, queryOptions);
302356
} catch (err) {
303357
throw convertError(err);
304358
}
@@ -312,7 +366,7 @@ class Statement {
312366
iterate(...bindParameters) {
313367
try {
314368
const { params, queryOptions } = splitBindParameters(bindParameters);
315-
const it = statementIterateSync(this.stmt, params, queryOptions);
369+
const it = statementIterateSync(this._getNativeStatement(), params, queryOptions);
316370
return {
317371
next: () => iteratorNextSync(it),
318372
[Symbol.iterator]() {
@@ -349,26 +403,31 @@ class Statement {
349403
* Interrupts the statement.
350404
*/
351405
interrupt() {
352-
this.stmt.interrupt();
406+
this._getNativeStatement().interrupt();
353407
return this;
354408
}
355409

356410
/**
357411
* Returns the columns in the result set returned by this prepared statement.
358412
*/
359413
columns() {
360-
return this.stmt.columns();
414+
return this._getNativeStatement().columns();
361415
}
362416

363417
/**
364418
* Toggle 64-bit integer support.
365419
*/
366420
safeIntegers(toggle) {
367-
this.stmt.safeIntegers(toggle);
421+
this._getNativeStatement().safeIntegers(toggle);
368422
return this;
369423
}
370424
}
371425

426+
if (symbolDispose != null) {
427+
Database.prototype[symbolDispose] = Database.prototype.close;
428+
Statement.prototype[symbolDispose] = Statement.prototype.close;
429+
}
430+
372431
module.exports = Database;
373432
module.exports.SqliteError = SqliteError;
374433
module.exports.Authorization = Authorization;

docs/api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Cancel ongoing operations and make them return at earliest opportunity.
116116
### close() ⇒ this
117117

118118
Closes the database connection.
119+
All statements created from this database are closed as well.
119120

120121
# class Statement
121122

@@ -159,6 +160,10 @@ Executes the SQL statement and returns an iterator to the resulting rows.
159160
| bindParameters | <code>array of objects</code> | The bind parameters for executing the statement. |
160161
| queryOptions | <code>object</code> | Optional per-query overrides (for example, `{ queryTimeout: 100 }`). |
161162

163+
### close() ⇒ this
164+
165+
Closes the prepared statement and releases its resources.
166+
162167
### pluck([toggleState]) ⇒ this
163168

164169
This function is currently not supported.

index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ export declare class Statement {
183183
columns(): unknown[]
184184
safeIntegers(toggle?: boolean | undefined | null): this
185185
interrupt(): void
186+
/** Closes the statement. */
187+
close(): void
186188
}
187189
/** A raw iterator over rows. The JavaScript layer wraps this in a iterable. */
188190
export declare class RowsIterator {

integration-tests/tests/async.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,30 @@ test.serial("Database.exec() after close()", async (t) => {
340340
});
341341
});
342342

343+
test.serial("Statement.get() after Database.close()", async (t) => {
344+
const db = t.context.db;
345+
const stmt = await db.prepare("SELECT 1");
346+
db.close();
347+
await t.throwsAsync(async () => {
348+
await stmt.get();
349+
}, {
350+
instanceOf: TypeError,
351+
message: "The database connection is not open"
352+
});
353+
});
354+
355+
test.serial("Statement.close()", async (t) => {
356+
const db = t.context.db;
357+
const stmt = await db.prepare("SELECT 1");
358+
stmt.close();
359+
await t.throwsAsync(async () => {
360+
await stmt.get();
361+
}, {
362+
instanceOf: TypeError,
363+
message: "The database connection is not open"
364+
});
365+
});
366+
343367
test.serial("Database.interrupt()", async (t) => {
344368
const db = t.context.db;
345369
const stmt = await db.prepare("WITH RECURSIVE infinite_loop(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM infinite_loop) SELECT * FROM infinite_loop;");

integration-tests/tests/sync.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,34 @@ test.serial("Database.exec() after close()", async (t) => {
433433
});
434434
});
435435

436+
test.serial("Statement.get() after Database.close()", async (t) => {
437+
const db = t.context.db;
438+
const stmt = db.prepare("SELECT 1");
439+
db.close();
440+
t.throws(() => {
441+
stmt.get();
442+
}, {
443+
instanceOf: TypeError,
444+
message: "The database connection is not open"
445+
});
446+
});
447+
448+
test.serial("Statement.close()", async (t) => {
449+
const db = t.context.db;
450+
const stmt = db.prepare("SELECT 1");
451+
if (typeof stmt.close !== "function") {
452+
t.pass();
453+
return;
454+
}
455+
stmt.close();
456+
t.throws(() => {
457+
stmt.get();
458+
}, {
459+
instanceOf: TypeError,
460+
message: "The database connection is not open"
461+
});
462+
});
463+
436464
test.serial("Timeout option", async (t) => {
437465
const timeout = 1000;
438466
const path = genDatabaseFilename();

0 commit comments

Comments
 (0)