From c07af3980fa3ed10c492afba05b842fb3abb4c55 Mon Sep 17 00:00:00 2001 From: Paul Bouchon Date: Sun, 9 Aug 2026 12:08:13 -0400 Subject: [PATCH] sqlite: improve error for excess bound parameters Passing more values than a prepared statement has parameters surfaced SQLite's own "column index out of range" error. That wording describes a binding index, but reads to a JavaScript caller as a problem with the table's columns rather than with the call, and it is inconsistent with the adjacent failure: binding a value of the wrong type already throws a Node-authored ERR_INVALID_ARG_VALUE from the same function. Detect the overflow before handing the index to sqlite3_bind_*() and throw an error that names the actual problem. The check runs after the scan for the next anonymous slot, so it also covers excess anonymous values passed alongside named parameters. This changes the error thrown for an existing case, so it needs semver-major treatment. Fixes: https://github.com/nodejs/node/issues/65163 Signed-off-by: Paul Bouchon --- src/node_sqlite.cc | 15 +++++++++ test/parallel/test-sqlite-data-types.js | 35 +++++++++++++++++++++ test/parallel/test-sqlite-statement-sync.js | 8 ++--- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 8e2f13cd6334..ade13640dce5 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -2702,6 +2702,8 @@ bool StatementSync::BindParams(const FunctionCallbackInfo& args) { anon_start++; } + int param_count = sqlite3_bind_parameter_count(statement_); + for (int i = anon_start; i < args.Length(); ++i) { while (1) { const char* param = sqlite3_bind_parameter_name(statement_, anon_idx); @@ -2709,6 +2711,19 @@ bool StatementSync::BindParams(const FunctionCallbackInfo& args) { anon_idx++; } + // More values were supplied than the statement has parameters to bind + // them to. Report that directly instead of letting sqlite3_bind_*() + // surface its "column index out of range" error, which reads as a + // problem with the table rather than with the call. + if (anon_idx > param_count) { + THROW_ERR_INVALID_ARG_VALUE( + env(), + "Too many parameter values were provided. The statement accepts %d " + "parameter(s), which are already bound.", + param_count); + return false; + } + if (!BindValue(args[i], anon_idx)) { return false; } diff --git a/test/parallel/test-sqlite-data-types.js b/test/parallel/test-sqlite-data-types.js index 26af15a777d2..718a299266a2 100644 --- a/test/parallel/test-sqlite-data-types.js +++ b/test/parallel/test-sqlite-data-types.js @@ -151,6 +151,41 @@ suite('data binding and mapping', () => { }); }); + test('throws when more values are provided than the statement accepts', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY) STRICT;'); + + t.assert.throws(() => { + db.prepare('INSERT INTO data (key) VALUES (?)').run(1, 2); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /Too many parameter values were provided.+accepts 1 parameter/, + }); + + // A statement with no parameters at all. + t.assert.throws(() => { + db.prepare('SELECT 1').get(5); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /Too many parameter values were provided.+accepts 0 parameter/, + }); + + // Excess anonymous values alongside named parameters. + t.assert.throws(() => { + db.prepare('INSERT INTO data (key) VALUES ($k)').run({ $k: 1 }, 2); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /Too many parameter values were provided.+accepts 1 parameter/, + }); + + // The correct number of values still binds. + t.assert.deepStrictEqual( + db.prepare('INSERT INTO data (key) VALUES (?)').run(10), + { changes: 1, lastInsertRowid: 10 }, + ); + }); + test('throws when binding a BigInt that is too large', (t) => { const max = 9223372036854775807n; // Largest 64-bit signed integer value. const db = new DatabaseSync(nextDb()); diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index b3a1dc434537..de32a2c42c73 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -310,7 +310,7 @@ suite('StatementSync.prototype.run()', () => { t.assert.deepStrictEqual(stmt.run(), { changes: 1, lastInsertRowid: 1 }); }); - test('SQLite throws when trying to bind too many parameters', (t) => { + test('throws when trying to bind too many parameters', (t) => { const db = new DatabaseSync(nextDb()); t.after(() => { db.close(); }); const setup = db.exec( @@ -321,10 +321,8 @@ suite('StatementSync.prototype.run()', () => { t.assert.throws(() => { stmt.run(1, 2, 3); }, { - code: 'ERR_SQLITE_ERROR', - message: 'column index out of range', - errcode: 25, - errstr: 'column index out of range', + code: 'ERR_INVALID_ARG_VALUE', + message: /Too many parameter values were provided.+accepts 2 parameter/, }); });