Commit 9bf40b24cd8 for nodejs

commit 9bf40b24cd8b1975ad546e1fe3294e0bbd029b47
Author: Trivikram Kamat <trivikr.dev@gmail.com>
Date:   Tue Sep 29 16:36:16 2026 -0700

    sqlite: reject plans with unusable virtual table parameters

    Return SQLITE_CONSTRAINT from xBestIndex() when a hidden column has
    an equality constraint that is not usable at the current point in the
    query plan. Accepting such a plan makes xFilter() call rows() with
    null, silently dropping rows and returning an empty result, e.g. for
    "SELECT DISTINCT * FROM m(t.a) JOIN t ON t.a = m.a". Cost estimates
    alone cannot enforce parameter dependencies; rejecting the plan lets
    SQLite pick an ordering where the parameter value is available.

    Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com>
    Assisted-by: opencode
    PR-URL: https://github.com/nodejs/node/pull/66215
    Fixes: https://github.com/nodejs/node/issues/66214
    Reviewed-By: Guilherme Araújo <arauujogui@gmail.com>

diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc
index e73bbf12cd5..a243ec49c4d 100644
--- a/src/node_sqlite.cc
+++ b/src/node_sqlite.cc
@@ -1201,6 +1201,7 @@ int VirtualTableModule::xBestIndex(sqlite3_vtab* pVTab,
   for (int hidden_idx = 0; hidden_idx < num_hidden; hidden_idx++) {
     int col = mod->hidden_col_indices_[hidden_idx];

+    int args_before = argv_index;
     for (int i = 0; i < pInfo->nConstraint; i++) {
       if (pInfo->aConstraint[i].iColumn == col &&
           pInfo->aConstraint[i].usable &&
@@ -1215,6 +1216,20 @@ int VirtualTableModule::xBestIndex(sqlite3_vtab* pVTab,
         break;
       }
     }
+
+    // No usable constraint means the query supplied a parameter whose value is
+    // not available at this point in the plan. Accepting it would pass null to
+    // rows() and silently return empty results, so reject the plan and let
+    // SQLite pick a different ordering instead.
+    // https://www.sqlite.org/vtab.html#enforcing_required_parameters_on_table_valued_functions
+    if (argv_index == args_before) {
+      for (int i = 0; i < pInfo->nConstraint; i++) {
+        if (pInfo->aConstraint[i].iColumn == col &&
+            pInfo->aConstraint[i].op == SQLITE_INDEX_CONSTRAINT_EQ) {
+          return SQLITE_CONSTRAINT;
+        }
+      }
+    }
   }

   if (!idx_str.empty()) {
diff --git a/test/parallel/test-sqlite-virtual-table.js b/test/parallel/test-sqlite-virtual-table.js
index 4dc421618fd..3547fa7e765 100644
--- a/test/parallel/test-sqlite-virtual-table.js
+++ b/test/parallel/test-sqlite-virtual-table.js
@@ -1,6 +1,6 @@
 // Flags: --expose-gc
 'use strict';
-const { skipIfSQLiteMissing } = require('../common');
+const { skipIfSQLiteMissing, mustCallAtLeast } = require('../common');
 skipIfSQLiteMissing();
 const assert = require('node:assert');
 const { Database } = require('node:sqlite');
@@ -345,6 +345,40 @@ suite('Database.prototype.createModule()', () => {
       assert.strictEqual(received.length, paramCount);
       assert.strictEqual(received[paramCount - 1], 7);
     });
+
+    test('does not pass null for parameters that are unavailable in a plan', () => {
+      const db = new DatabaseSync(':memory:');
+
+      db.createModule('join_params', {
+        columns: [
+          { name: 'value', type: 'INTEGER' },
+          { name: 'param', type: 'INTEGER', hidden: true },
+        ],
+        rows: mustCallAtLeast(function*(param) {
+          assert.notStrictEqual(param, null,
+                                'rows() must not be called with an unavailable ' +
+                                'parameter');
+          if (param !== null) {
+            yield [param];
+          }
+        }),
+      });
+
+      db.exec('CREATE TABLE t (a INTEGER)');
+      db.exec('INSERT INTO t VALUES (1), (2), (3)');
+
+      // With DISTINCT, SQLite may consider a plan where the parameter is read
+      // from the inner table and is not yet available, which used to make
+      // xBestIndex accept it and call rows(null), producing an empty result.
+      const result = db.prepare(
+        'SELECT DISTINCT value FROM join_params, t WHERE join_params.param = t.a'
+      ).all();
+      assert.deepStrictEqual(result, [
+        { __proto__: null, value: 1 },
+        { __proto__: null, value: 2 },
+        { __proto__: null, value: 3 },
+      ]);
+    });
   });

   suite('type conversions', () => {