From 545b5684aa21776e6a8eed4f9dfe727d3bec8491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Thu, 26 Mar 2026 15:39:15 +0100 Subject: [PATCH 1/3] Introduce Prepared Info class This class aims to replace the current mess of types that were used for representing prepared statement information across the JS code. This will be useful in the next commit(s) that will introduce named parameters. --- lib/client.js | 65 +++++++++++++++-------------------------- lib/concurrent/index.js | 3 +- lib/prepared.js | 26 +++++++++++++++++ 3 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 lib/prepared.js diff --git a/lib/client.js b/lib/client.js index dfff6fc8c..8c8e913d8 100644 --- a/lib/client.js +++ b/lib/client.js @@ -20,7 +20,7 @@ const { const promiseUtils = require("./promise-utils"); const rust = require("../index"); const ResultSet = require("./types/result-set.js"); -const { encodeParams, convertComplexType } = require("./types/cql-utils.js"); +const { encodeParams } = require("./types/cql-utils.js"); const { PreparedCache } = require("./cache.js"); const Encoder = require("./encoder.js"); const { HostMap } = require("./host.js"); @@ -28,6 +28,7 @@ const { HostMap } = require("./host.js"); // Imports for the purpose of type hints in JS docs. // eslint-disable-next-line no-unused-vars const { QueryOptions } = require("./query-options.js"); +const { PreparedInfo } = require("./prepared.js"); /** * Represents a database client that maintains multiple connections to the cluster nodes, providing methods to @@ -164,19 +165,6 @@ class Client extends events.EventEmitter { return fullOptions; } - /** - * Manually prepare query into prepared statement - * @param {string} query - * @returns {Promise>} - * Returns a tuple of type object (the format expected by the encoder) and prepared statement wrapper - * @package - */ - async prepareQuery(query) { - let expectedTypes = await this.rustClient.prepareStatement(query); - let res = [expectedTypes.map((t) => convertComplexType(t)), query]; - return res; - } - /** * Attempts to connect to one of the [contactPoints]{@link ClientOptions} and discovers the rest the nodes of the * cluster. @@ -318,7 +306,7 @@ class Client extends events.EventEmitter { /** * Wrapper for executing queries by rust driver - * @param {string | list} query + * @param {string | PreparedInfo} query * @param {Array} params * @param {ExecutionOptions} execOptions * @returns {Promise} @@ -348,26 +336,21 @@ class Client extends events.EventEmitter { let result; if (execOptions.isPrepared()) { + /** + * @type {PreparedInfo} + */ + let prepared = query; // If the statement is already prepared, skip the preparation process // Otherwise call Rust part to prepare a statement if (typeof query === "string") { - query = await this.prepareQuery(query); + prepared = await PreparedInfo.create(query, this.rustClient); } - /** - * @type {string} - */ - let statement = query[1]; - /** - * @type {Object} - */ - let expectedTypes = query[0]; - - let encoded = encodeParams(expectedTypes, params, this.#encoder); + let encoded = encodeParams(prepared.types, params, this.#encoder); // Execute query result = await this.rustClient.executePreparedUnpagedEncoded( - statement, + prepared.query, encoded, rustOptions, ); @@ -554,26 +537,21 @@ class Client extends events.EventEmitter { const rustOptions = execOptions.getRustOptions(); let result; if (execOptions.isPrepared()) { + /** + * @type {PreparedInfo} + */ + let prepared = query; // If the statement is already prepared, skip the preparation process // Otherwise call Rust part to prepare a statement if (typeof query === "string") { - query = await this.prepareQuery(query); + prepared = await PreparedInfo.create(query, this.rustClient); } - /** - * @type {string} - */ - let statement = query[1]; - /** - * @type {Object} - */ - let expectedTypes = query[0]; - - let encoded = encodeParams(expectedTypes, params, this.#encoder); + let encoded = encodeParams(prepared.types, params, this.#encoder); // Execute query result = await this.rustClient.executeSinglePageEncoded( - statement, + prepared.query, encoded, rustOptions, pageState, @@ -749,11 +727,14 @@ class Client extends events.EventEmitter { if (shouldBePrepared) { let prepared = preparedCache.getElement(statement); if (!prepared) { - prepared = await this.prepareQuery(statement); + prepared = await PreparedInfo.create( + statement, + this.rustClient, + ); preparedCache.storeElement(statement, prepared); } - types = prepared[0]; - statement = prepared[1]; + types = prepared.types; + statement = prepared.query; } else { types = hints[i] || []; } diff --git a/lib/concurrent/index.js b/lib/concurrent/index.js index 72ee093fb..0590854f5 100644 --- a/lib/concurrent/index.js +++ b/lib/concurrent/index.js @@ -4,6 +4,7 @@ const _Client = require("../client"); const utils = require("../utils"); const { Stream } = require("stream"); const { PreparedCache } = require("../cache"); +const { PreparedInfo } = require("../prepared"); /** * Utilities for concurrent query execution with the DataStax Node.js Driver. @@ -155,7 +156,7 @@ class ArrayBasedExecutor { try { let prepared = this._cache.getElement(query); if (!prepared) { - prepared = await (this._client.prepareQuery(query)); + prepared = await PreparedInfo.create(query, this._client.rustClient); this._cache.storeElement(query, prepared); } await this._client diff --git a/lib/prepared.js b/lib/prepared.js new file mode 100644 index 000000000..9bb4f0504 --- /dev/null +++ b/lib/prepared.js @@ -0,0 +1,26 @@ +const _rust = require("../index"); +const { convertComplexType } = require("./types/cql-utils"); + +class PreparedInfo { + /** + * @param {Array} types + * @param {string} query + */ + constructor(types, query) { + this.types = types; + this.query = query; + } + + /** + * @param {string} query + * @param {_rust.SessionWrapper} client + * @returns {Promise} + */ + static async create(query, client) { + let expectedTypes = await client.prepareStatement(query); + let types = expectedTypes.map((t) => convertComplexType(t)); + return new PreparedInfo(types, query); + } +} + +module.exports.PreparedInfo = PreparedInfo; From 63989e03ef850e540a14239819e63facf00a3b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Thu, 26 Mar 2026 17:02:17 +0100 Subject: [PATCH 2/3] Named parameters in prepared statements This commit introduces support for named parameters in prepared statements. The desired behavior is based on DSx integration tests: When an object is passed as params instead of an array, the driver maps parameter names to positional indices using column metadata from prepared info. Lookup is case-insensitive; extra properties not matching any column are silently ignored. Supported in execute(), eachRow()/stream() (paged), and batch() with { prepare: true }. --- lib/client.js | 44 +++++++------------ lib/new-utils.js | 13 ++++++ lib/prepared.js | 9 ++-- lib/utils.js | 14 ++---- src/requests/request.rs | 11 +++-- src/session.rs | 11 ++--- .../supported/client-batch-tests.js | 7 ++- .../client-execute-prepared-tests.js | 7 +-- 8 files changed, 59 insertions(+), 57 deletions(-) diff --git a/lib/client.js b/lib/client.js index 8c8e913d8..adbffc393 100644 --- a/lib/client.js +++ b/lib/client.js @@ -2,7 +2,7 @@ const events = require("events"); const util = require("util"); -const { throwNotSupported } = require("./new-utils.js"); +const { throwNotSupported, isNamedParameters } = require("./new-utils.js"); const utils = require("./utils.js"); const errors = require("./errors.js"); @@ -307,24 +307,13 @@ class Client extends events.EventEmitter { /** * Wrapper for executing queries by rust driver * @param {string | PreparedInfo} query - * @param {Array} params + * @param {Array | Object} params * @param {ExecutionOptions} execOptions * @returns {Promise} * @package */ async rustyExecute(query, params, execOptions) { - if ( - // !execOptions.isPrepared() && - params && - !Array.isArray(params) - // && !types.protocolVersion.supportsNamedParameters(version) - ) { - throw new Error(`TODO: Implement any support for named parameters`); - // // Only Cassandra 2.1 and above supports named parameters - // throw new errors.ArgumentError( - // "Named parameters for simple statements are not supported, use prepare flag", - // ); - } + let withNamedParameters = isNamedParameters(params, execOptions); if (!this.connected) { // TODO: Check this logic and decide if it's needed. Probably do it while implementing (better) connection @@ -345,6 +334,9 @@ class Client extends events.EventEmitter { if (typeof query === "string") { prepared = await PreparedInfo.create(query, this.rustClient); } + if (withNamedParameters) { + params = utils.adaptNamedParamsPrepared(params, prepared); + } let encoded = encodeParams(prepared.types, params, this.#encoder); @@ -494,25 +486,14 @@ class Client extends events.EventEmitter { /** * Execute a single page of query * @param {string} query - * @param {Array} params + * @param {Array | Object} params * @param {ExecutionOptions} execOptions * @param {rust.PagingStateWrapper|Buffer} [pageState] * @returns {Promise>} should be Promise<[rust.PagingStateResponseWrapper, ResultSet]> * @private */ async #rustyPaged(query, params, execOptions, pageState) { - if ( - !execOptions.isPrepared() && - params && - !Array.isArray(params) - // && !types.protocolVersion.supportsNamedParameters(version) - ) { - throw new Error(`TODO: Implement any support for named parameters`); - // // Only Cassandra 2.1 and above supports named parameters - // throw new errors.ArgumentError( - // "Named parameters for simple statements are not supported, use prepare flag", - // ); - } + let withNamedParameters = isNamedParameters(params, execOptions); if (!this.connected) { // TODO: Check this logic and decide if it's needed. Probably do it while implementing (better) connection @@ -547,6 +528,10 @@ class Client extends events.EventEmitter { prepared = await PreparedInfo.create(query, this.rustClient); } + if (withNamedParameters) { + params = utils.adaptNamedParamsPrepared(params, prepared); + } + let encoded = encodeParams(prepared.types, params, this.#encoder); // Execute query @@ -735,6 +720,10 @@ class Client extends events.EventEmitter { } types = prepared.types; statement = prepared.query; + + if (params && !Array.isArray(params)) { + params = utils.adaptNamedParamsPrepared(params, prepared); + } } else { types = hints[i] || []; } @@ -831,6 +820,7 @@ class Client extends events.EventEmitter { * @returns {void} */ } + /** * Callback used by execution methods. * @callback ResultCallback diff --git a/lib/new-utils.js b/lib/new-utils.js index dc282d0a5..763d12b39 100644 --- a/lib/new-utils.js +++ b/lib/new-utils.js @@ -137,10 +137,23 @@ function ensure64SignedInteger(number, name) { } } +function isNamedParameters(params, execOptions) { + if (params && !Array.isArray(params)) { + if (!execOptions.isPrepared()) { + throw new customErrors.ArgumentError( + "Named parameters for simple statements are not supported, use prepare flag", + ); + } + return true; + } + return false; +} + exports.throwNotSupported = throwNotSupported; exports.napiErrorHandler = napiErrorHandler; exports.throwNotSupported = throwNotSupported; exports.bigintToLong = bigintToLong; exports.arbitraryValueToBigInt = arbitraryValueToBigInt; +exports.isNamedParameters = isNamedParameters; exports.ensure32SignedInteger = ensure32SignedInteger; exports.ensure64SignedInteger = ensure64SignedInteger; diff --git a/lib/prepared.js b/lib/prepared.js index 9bb4f0504..542bcaae8 100644 --- a/lib/prepared.js +++ b/lib/prepared.js @@ -5,10 +5,12 @@ class PreparedInfo { /** * @param {Array} types * @param {string} query + * @param {Array} colNames */ - constructor(types, query) { + constructor(types, query, colNames) { this.types = types; this.query = query; + this.colNames = colNames; } /** @@ -18,8 +20,9 @@ class PreparedInfo { */ static async create(query, client) { let expectedTypes = await client.prepareStatement(query); - let types = expectedTypes.map((t) => convertComplexType(t)); - return new PreparedInfo(types, query); + let types = expectedTypes.map((t) => convertComplexType(t[0])); + let colNames = expectedTypes.map((t) => t[1].toLowerCase()); + return new PreparedInfo(types, query, colNames); } } diff --git a/lib/utils.js b/lib/utils.js index 123a0e67e..231855f3c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -288,26 +288,20 @@ function validateFn(fn, name) { * If the params are passed as an associative array (Object), * it adapts the object into an array with the same order as columns * @param {Array|Object} params - * @param {Array} columns + * @param {PreparedInfo} columns * @returns {Array} Returns an array of parameters. * @throws {Error} In case a parameter with a specific name is not defined */ function adaptNamedParamsPrepared(params, columns) { - if (!params || Array.isArray(params) || !columns || columns.length === 0) { - // params is an array or there aren't parameters - return params; - } - const paramsArray = new Array(columns.length); + const paramsArray = new Array(columns.types.length); params = toLowerCaseProperties(params); - const keys = {}; - for (let i = 0; i < columns.length; i++) { - const name = columns[i].name; + for (let i = 0; i < columns.types.length; i++) { + const name = columns.colNames[i]; if (!Object.prototype.hasOwnProperty.call(params, name)) { throw new errors.ArgumentError(`Parameter "${name}" not defined`); } paramsArray[i] = params[name]; - keys[name] = i; } return paramsArray; } diff --git a/src/requests/request.rs b/src/requests/request.rs index 5f3bec706..d304b7111 100644 --- a/src/requests/request.rs +++ b/src/requests/request.rs @@ -51,12 +51,17 @@ impl QueryOptionsWrapper { } impl PreparedStatementWrapper { - /// Get array of expected types for this prepared statement. - pub fn get_expected_types(&self) -> Vec> { + /// Get array of (expected type, variable name) pairs for this prepared statement. + pub fn get_expected_types(&self) -> Vec<(ComplexType<'static>, String)> { self.prepared .get_variable_col_specs() .iter() - .map(|e| ComplexType::new_owned(e.typ().clone())) + .map(|e| { + ( + ComplexType::new_owned(e.typ().clone()), + e.name().to_string(), + ) + }) .collect() } } diff --git a/src/session.rs b/src/session.rs index 190dca995..0bb45434a 100644 --- a/src/session.rs +++ b/src/session.rs @@ -162,13 +162,13 @@ impl SessionWrapper { .await } - /// Prepares a statement through rust driver for a given session - /// Return expected types for the prepared statement - #[napi(ts_return_type = "Promise>")] + /// Prepares a statement through rust driver for a given session. + /// Returns (expected type, variable name) pairs for the prepared statement. + #[napi(ts_return_type = "Promise>")] pub async fn prepare_statement( &self, statement: String, - ) -> JsResult>> { + ) -> JsResult, String)>> { with_custom_error_async(async || { let statement: Statement = statement.into(); let w = PreparedStatementWrapper { @@ -177,7 +177,8 @@ impl SessionWrapper { .add_prepared_statement(&statement) // TODO: change for add_prepared_statement_to_owned after it is made public .await?, }; - ConvertedResult::Ok(w.get_expected_types()) + let types = w.get_expected_types(); + ConvertedResult::Ok(types) }) .await } diff --git a/test/integration/supported/client-batch-tests.js b/test/integration/supported/client-batch-tests.js index b43821d62..2807c275c 100644 --- a/test/integration/supported/client-batch-tests.js +++ b/test/integration/supported/client-batch-tests.js @@ -914,9 +914,7 @@ describe("Client @SERVER_API", function () { ); }, ); - // No support for named parameters - // TODO: Fix this test - /* vit("2.0", "should allow named parameters", function (done) { + vit("2.0", "should allow named parameters", function (done) { const client = newInstance(); const id1 = types.Uuid.random(); const id2 = types.Uuid.random(); @@ -928,6 +926,7 @@ describe("Client @SERVER_API", function () { table1, ), params: { + // eslint-disable-next-line camelcase text_SAMPLE: "named params", paramID: id1, time: types.TimeUuid.now(), @@ -990,7 +989,7 @@ describe("Client @SERVER_API", function () { ); }, ); - }); */ + }); vit( "2.0", diff --git a/test/integration/supported/client-execute-prepared-tests.js b/test/integration/supported/client-execute-prepared-tests.js index 165633741..2868fd4b0 100644 --- a/test/integration/supported/client-execute-prepared-tests.js +++ b/test/integration/supported/client-execute-prepared-tests.js @@ -540,10 +540,7 @@ describe("Client @SERVER_API", function () { done, ); }); - - // No support for named parameters - // TODO: fix this test - /* describe("with named parameters", function () { + describe("with named parameters", function () { vit("2.0", "should allow an array of parameters", function (done) { const query = util.format( "SELECT * FROM %s WHERE id1 = :id1", @@ -626,7 +623,7 @@ describe("Client @SERVER_API", function () { ); }, ); - }); */ + }); it("should encode and decode maps using Map polyfills", function (done) { const client = newInstance({ From cd951b669bd3f9389ec715dcebebdf6f4a8d89d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Fri, 27 Mar 2026 13:08:24 +0100 Subject: [PATCH 3/3] Remove duplicate export in new utils --- lib/new-utils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/new-utils.js b/lib/new-utils.js index 763d12b39..54cdb52c1 100644 --- a/lib/new-utils.js +++ b/lib/new-utils.js @@ -151,7 +151,6 @@ function isNamedParameters(params, execOptions) { exports.throwNotSupported = throwNotSupported; exports.napiErrorHandler = napiErrorHandler; -exports.throwNotSupported = throwNotSupported; exports.bigintToLong = bigintToLong; exports.arbitraryValueToBigInt = arbitraryValueToBigInt; exports.isNamedParameters = isNamedParameters;