diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index 2f3111432be5a6..e4ba433e913ef0 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -631,10 +631,14 @@ added: v22.5.0 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])` @@ -1625,6 +1629,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 @@ -1649,7 +1654,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 4d1a35d753230a..2b4700cdb02ea4 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -1441,6 +1441,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()) { @@ -1521,11 +1522,31 @@ 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); CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); BaseObjectPtr stmt = @@ -3531,8 +3552,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); if (r != SQLITE_OK) { THROW_ERR_SQLITE_ERROR(isolate, session->database_.get()); diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index aa7a3a73ae6649..842faea14cc434 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -909,3 +909,46 @@ suite('options.allowBareNamedParameters', () => { ); }); }); + +suite('options.persistent', () => { + test('statement executes correctly when persistent is true', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'); + db.exec('INSERT INTO data (key, val) VALUES (1, 42);'); + const 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) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'); + db.exec('INSERT INTO data (key, val) VALUES (1, 42);'); + const 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) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + 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) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'); + db.exec('INSERT INTO data (key, val) VALUES (1, 42);'); + const stmt = db.prepare( + 'SELECT val FROM data', + { persistent: true, readBigInts: true } + ); + t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42n }); + }); +});