diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index e50a66e92621..5f4eea61e453 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -690,10 +690,14 @@ changes: database options or `true`. * `allowUnknownNamedParameters` {boolean} If `true`, unknown named parameters are ignored. **Default:** inherited from database options or `false`. + * `persistent` {boolean} If `true`, hints to SQLite that this statement will + be reused many times, causing it to use a different memory allocation + strategy that reduces heap fragmentation. Corresponds to the + [`SQLITE_PREPARE_PERSISTENT`][] flag. **Default:** `false`. * Returns: {StatementSync} The prepared statement. Compiles a SQL statement into a [prepared statement][]. This method is a wrapper -around [`sqlite3_prepare_v2()`][]. +around [`sqlite3_prepare_v3()`][]. ### `database.createTagStore([maxSize])` @@ -1860,6 +1864,7 @@ callback function to indicate what type of operation is being authorized. [`SQLITE_DETERMINISTIC`]: https://www.sqlite.org/c3ref/c_deterministic.html [`SQLITE_DIRECTONLY`]: https://www.sqlite.org/c3ref/c_deterministic.html [`SQLITE_MAX_FUNCTION_ARG`]: https://www.sqlite.org/limits.html#max_function_arg +[`SQLITE_PREPARE_PERSISTENT`]: https://sqlite.org/c3ref/c_prepare_dont_log.html#sqlitepreparepersistent [`SQLTagStore`]: #class-sqltagstore [`database.applyChangeset()`]: #databaseapplychangesetchangeset-options [`database.createTagStore()`]: #databasecreatetagstoremaxsize @@ -1885,7 +1890,7 @@ callback function to indicate what type of operation is being authorized. [`sqlite3_get_autocommit()`]: https://sqlite.org/c3ref/get_autocommit.html [`sqlite3_last_insert_rowid()`]: https://www.sqlite.org/c3ref/last_insert_rowid.html [`sqlite3_load_extension()`]: https://www.sqlite.org/c3ref/load_extension.html -[`sqlite3_prepare_v2()`]: https://www.sqlite.org/c3ref/prepare.html +[`sqlite3_prepare_v3()`]: https://www.sqlite.org/c3ref/prepare.html [`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html [`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html [`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 156d4f2e5f5c..0deb4c0a4b0f 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -1510,6 +1510,7 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { std::optional use_big_ints; std::optional allow_bare_named_params; std::optional allow_unknown_named_params; + std::optional persistent; if (args.Length() > 1 && !args[1]->IsUndefined()) { if (!args[1]->IsObject()) { @@ -1590,11 +1591,34 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { } allow_unknown_named_params = allow_unknown_named_params_v->IsTrue(); } + + Local persistent_v; + if (!options + ->Get(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "persistent")) + .ToLocal(&persistent_v)) { + return; + } + if (!persistent_v->IsUndefined()) { + if (!persistent_v->IsBoolean()) { + THROW_ERR_INVALID_ARG_TYPE( + env->isolate(), + "The \"options.persistent\" argument must be a boolean."); + return; + } + persistent = persistent_v->IsTrue(); + } } Utf8Value sql(env->isolate(), args[0].As()); sqlite3_stmt* s = nullptr; - int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, nullptr); + + unsigned int prep_flags = + persistent.value_or(false) ? SQLITE_PREPARE_PERSISTENT : 0; + + int r = + sqlite3_prepare_v3(db->connection_, *sql, -1, prep_flags, &s, nullptr); + StatementPtr stmt_ptr(s); CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); @@ -3766,8 +3790,12 @@ BaseObjectPtr SQLTagStore::PrepareStatement( if (stmt == nullptr) { sqlite3_stmt* s = nullptr; - int r = sqlite3_prepare_v2( - session->database_->connection_, sql.data(), sql.size(), &s, nullptr); + int r = sqlite3_prepare_v3(session->database_->connection_, + sql.data(), + sql.size(), + SQLITE_PREPARE_PERSISTENT, + &s, + nullptr); StatementPtr stmt_ptr(s); if (r != SQLITE_OK) { diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index a52634f59ac6..b13da41e9d71 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -18,8 +18,7 @@ suite('StatementSync() constructor', () => { suite('StatementSync.prototype.get()', () => { test('executes a query and returns undefined on no results', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); t.assert.strictEqual(stmt.get(), undefined); stmt = db.prepare('SELECT * FROM storage'); @@ -27,8 +26,7 @@ suite('StatementSync.prototype.get()', () => { }); test('executes a query and returns the first result', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); t.assert.strictEqual(stmt.get(), undefined); stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); @@ -39,8 +37,7 @@ suite('StatementSync.prototype.get()', () => { }); test('executes a query that returns special columns', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const stmt = db.prepare('SELECT 1 as __proto__, 2 as constructor, 3 as toString'); t.assert.deepStrictEqual(stmt.get(), { __proto__: null, ['__proto__']: 1, constructor: 2, toString: 3 }); }); @@ -105,15 +102,13 @@ suite('StatementSync.prototype.get()', () => { suite('StatementSync.prototype.all()', () => { test('executes a query and returns an empty array on no results', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); t.assert.deepStrictEqual(stmt.all(), []); }); test('executes a query and returns all results', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 }); stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); @@ -189,8 +184,7 @@ suite('StatementSync.prototype.all()', () => { suite('StatementSync.prototype.iterate()', () => { test('executes a query and returns an empty iterator on no results', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); const iter = stmt.iterate(); t.assert.strictEqual(iter instanceof globalThis.Iterator, true); @@ -199,8 +193,7 @@ suite('StatementSync.prototype.iterate()', () => { }); test('executes a query and returns all results', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 }); stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); @@ -402,8 +395,7 @@ suite('StatementSync.prototype.iterate()', () => { suite('StatementSync.prototype.run()', () => { test('executes a query and returns change metadata', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE storage(key TEXT, val TEXT); INSERT INTO storage (key, val) VALUES ('foo', 'bar'); @@ -414,8 +406,7 @@ suite('StatementSync.prototype.run()', () => { }); test('SQLite throws when trying to bind too many parameters', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -432,8 +423,7 @@ suite('StatementSync.prototype.run()', () => { }); test('SQLite defaults to NULL for unbound parameters', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;' ); @@ -469,8 +459,7 @@ suite('StatementSync.prototype.run()', () => { }); test('SQLite defaults unbound ?NNN parameters', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;' ); @@ -488,8 +477,7 @@ suite('StatementSync.prototype.run()', () => { }); test('binds ?NNN params by position', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;' ); @@ -513,8 +501,7 @@ suite('StatementSync.prototype.run()', () => { suite('StatementSync.prototype.sourceSQL', () => { test('equals input SQL', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -537,8 +524,7 @@ suite('StatementSync.prototype.sourceSQL', () => { suite('StatementSync.prototype.expandedSQL', () => { test('equals expanded SQL', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -772,8 +758,7 @@ suite('StatementSync.prototype.resetStats()', () => { suite('StatementSync.prototype.setReadBigInts()', () => { test('BigInts support can be toggled', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT; INSERT INTO data (key, val) VALUES (1, 42); @@ -805,8 +790,7 @@ suite('StatementSync.prototype.setReadBigInts()', () => { }); test('throws when input is not a boolean', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -821,8 +805,7 @@ suite('StatementSync.prototype.setReadBigInts()', () => { }); test('BigInt is required for reading large integers', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const bad = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`); t.assert.throws(() => { bad.get(); @@ -853,8 +836,7 @@ suite('StatementSync.prototype.setReadBigInts()', () => { suite('StatementSync.prototype.setReturnArrays()', () => { test('throws when input is not a boolean', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -883,8 +865,7 @@ suite('StatementSync.prototype.setReturnArrays()', () => { suite('StatementSync.prototype.get() with array output', () => { test('returns array row when setReturnArrays is true', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; INSERT INTO data (key, val) VALUES (1, 'one'); @@ -903,8 +884,7 @@ suite('StatementSync.prototype.get() with array output', () => { test('returns array rows with BigInts when both flags are set', (t) => { const expected = [1n, 9007199254740992n]; - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE big_data(id INTEGER, big_num INTEGER); INSERT INTO big_data VALUES (1, 9007199254740992); @@ -922,8 +902,7 @@ suite('StatementSync.prototype.get() with array output', () => { suite('StatementSync.prototype.all() with array output', () => { test('returns array rows when setReturnArrays is true', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; INSERT INTO data (key, val) VALUES (1, 'one'); @@ -963,8 +942,7 @@ suite('StatementSync.prototype.all() with array output', () => { 9, 'text3', ]; - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE wide_table( col1 INTEGER, col2 TEXT, col3 REAL, col4 BLOB, col5 INTEGER, @@ -988,8 +966,7 @@ suite('StatementSync.prototype.all() with array output', () => { suite('StatementSync.prototype.iterate() with array output', () => { test('iterates array rows when setReturnArrays is true', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; INSERT INTO data (key, val) VALUES (1, 'one'); @@ -1057,8 +1034,7 @@ suite('StatementSync.prototype.iterate() with array output', () => { suite('StatementSync.prototype.setAllowBareNamedParameters()', () => { test('bare named parameter support can be toggled', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -1083,8 +1059,7 @@ suite('StatementSync.prototype.setAllowBareNamedParameters()', () => { }); test('throws when input is not a boolean', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -1113,8 +1088,7 @@ suite('StatementSync.prototype.setAllowBareNamedParameters()', () => { suite('options.readBigInts', () => { test('BigInts are returned when input is true', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT; INSERT INTO data (key, val) VALUES (1, 42); @@ -1126,8 +1100,7 @@ suite('options.readBigInts', () => { }); test('numbers are returned when input is false', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT; INSERT INTO data (key, val) VALUES (1, 42); @@ -1139,8 +1112,7 @@ suite('options.readBigInts', () => { }); test('throws when input is not a boolean', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -1154,8 +1126,7 @@ suite('options.readBigInts', () => { }); test('setReadBigInts can override prepare option', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT; INSERT INTO data (key, val) VALUES (1, 42); @@ -1171,8 +1142,7 @@ suite('options.readBigInts', () => { suite('options.returnArrays', () => { test('arrays are returned when input is true', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; INSERT INTO data (key, val) VALUES (1, 'one'); @@ -1187,8 +1157,7 @@ suite('options.returnArrays', () => { }); test('objects are returned when input is false', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; INSERT INTO data (key, val) VALUES (1, 'one'); @@ -1203,8 +1172,7 @@ suite('options.returnArrays', () => { }); test('throws when input is not a boolean', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;' ); @@ -1218,8 +1186,7 @@ suite('options.returnArrays', () => { }); test('setReturnArrays can override prepare option', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; INSERT INTO data (key, val) VALUES (1, 'one'); @@ -1236,8 +1203,7 @@ suite('options.returnArrays', () => { }); test('all() returns arrays when input is true', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; INSERT INTO data (key, val) VALUES (1, 'one'); @@ -1256,8 +1222,7 @@ suite('options.returnArrays', () => { }); test('iterate() returns arrays when input is true', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; INSERT INTO data (key, val) VALUES (1, 'one'); @@ -1278,8 +1243,7 @@ suite('options.returnArrays', () => { suite('options.allowBareNamedParameters', () => { test('bare named parameters are allowed when input is true', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -1295,8 +1259,7 @@ suite('options.allowBareNamedParameters', () => { }); test('bare named parameters throw when input is false', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -1314,8 +1277,7 @@ suite('options.allowBareNamedParameters', () => { }); test('throws when input is not a boolean', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -1332,8 +1294,7 @@ suite('options.allowBareNamedParameters', () => { }); test('setAllowBareNamedParameters can override prepare option', (t) => { - const db = new DatabaseSync(':memory:'); - t.after(() => { db.close(); }); + using db = new DatabaseSync(':memory:'); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -1423,3 +1384,42 @@ suite('StatementSync.prototype[Symbol.dispose]()', () => { db.close(); }); }); + +suite('options.persistent', () => { + test('statement executes correctly when persistent is true', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'); + db.exec('INSERT INTO data (key, val) VALUES (1, 42);'); + using stmt = db.prepare('SELECT val FROM data', { persistent: true }); + t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42 }); + }); + + test('statement executes correctly when persistent is false', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'); + db.exec('INSERT INTO data (key, val) VALUES (1, 42);'); + using stmt = db.prepare('SELECT val FROM data', { persistent: false }); + t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42 }); + }); + + test('throws when input is not a boolean', (t) => { + using db = new DatabaseSync(':memory:'); + t.assert.throws(() => { + db.prepare('SELECT 1', { persistent: 'yes' }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.persistent" argument must be a boolean/, + }); + }); + + test('can be combined with other options', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'); + db.exec('INSERT INTO data (key, val) VALUES (1, 42);'); + using stmt = db.prepare( + 'SELECT val FROM data', + { persistent: true, readBigInts: true } + ); + t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42n }); + }); +});