From 79ed629ebe650d9d3498b3a83cef56d3e518ffd7 Mon Sep 17 00:00:00 2001 From: dualfroz Date: Sun, 18 Oct 2026 12:27:35 +0200 Subject: [PATCH] sqlite: avoid sqlite3_clear_bindings() before every run Since SQLite 3.54, sqlite3_clear_bindings() invalidates the query plan of the statement, so a statement was re-prepared on every run. Bind NULL only to the parameters that were not bound in this run instead, and skip clearing in the tag store, which always binds every parameter. Refs: https://github.com/nodejs/node/issues/66800 Signed-off-by: dualfroz --- src/node_sqlite.cc | 22 ++++++++++++------- test/parallel/test-sqlite-data-types.js | 28 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 705d9b4a79a5..924efcc3e76f 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -3545,8 +3545,10 @@ inline int Statement::ResetStatement() { } bool Statement::BindParams(const FunctionCallbackInfo& args) { - int r = sqlite3_clear_bindings(statement_.get()); - CHECK_ERROR_OR_THROW(env()->isolate(), db_.get(), r, SQLITE_OK, false); + int param_count = sqlite3_bind_parameter_count(statement_.get()); + // Parameter indexing starts at one. + MaybeStackBuffer bound(param_count + 1); + std::fill_n(bound.out(), param_count + 1, false); int anon_idx = 1; int anon_start = 0; @@ -3562,8 +3564,6 @@ bool Statement::BindParams(const FunctionCallbackInfo& args) { if (allow_bare_named_params_ && !bare_named_params_.has_value()) { bare_named_params_.emplace(); - int param_count = sqlite3_bind_parameter_count(statement_.get()); - // Parameter indexing starts at one. for (int i = 1; i <= param_count; ++i) { const char* name = sqlite3_bind_parameter_name(statement_.get(), i); if (name == nullptr) { @@ -3626,6 +3626,7 @@ bool Statement::BindParams(const FunctionCallbackInfo& args) { if (!BindValue(value, r)) { return false; } + bound[r] = true; } anon_start++; } @@ -3641,10 +3642,20 @@ bool Statement::BindParams(const FunctionCallbackInfo& args) { if (!BindValue(args[i], anon_idx)) { return false; } + bound[anon_idx] = true; anon_idx++; } + // sqlite3_clear_bindings() would expire statements whose query plan depends + // on a bound value, so only reset the parameters that were not bound. + for (int i = 1; i <= param_count; ++i) { + if (!bound[i]) { + int r = sqlite3_bind_null(statement_.get(), i); + CHECK_ERROR_OR_THROW(env()->isolate(), db_.get(), r, SQLITE_OK, false); + } + } + return true; } @@ -4406,9 +4417,6 @@ bool SQLTagStore::ResetAndBindStatement( int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, false); - r = sqlite3_clear_bindings(stmt->statement_.get()); - CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, false); - uint32_t n_params = args.Length() - 1; int param_count = sqlite3_bind_parameter_count(stmt->statement_.get()); if (param_count != static_cast(n_params)) { diff --git a/test/parallel/test-sqlite-data-types.js b/test/parallel/test-sqlite-data-types.js index 473e504ead26..3e4768c57148 100644 --- a/test/parallel/test-sqlite-data-types.js +++ b/test/parallel/test-sqlite-data-types.js @@ -256,4 +256,32 @@ suite('data binding and mapping', () => { [{ __proto__: null, key: 1, val: 5 }, { __proto__: null, key: 2, val: null }], ); }); + + test('omitted named parameters are unbound on each call', (t) => { + const db = new Database(':memory:'); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const named = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)'); + const mixed = db.prepare('INSERT INTO data (key, val) VALUES ($k, ?)'); + named.run({ k: 1, v: 5 }); + named.run({ k: 2 }); + named.run({ k: 3, v: 6 }); + named.run({ k: 4, $k: 4 }); + mixed.run({ k: 5 }, 7); + mixed.run({ k: 6 }); + t.assert.deepStrictEqual( + db.prepare('SELECT * FROM data ORDER BY key').all(), + [ + { __proto__: null, key: 1, val: 5 }, + { __proto__: null, key: 2, val: null }, + { __proto__: null, key: 3, val: 6 }, + { __proto__: null, key: 4, val: null }, + { __proto__: null, key: 5, val: 7 }, + { __proto__: null, key: 6, val: null }, + ], + ); + }); });