diff --git a/lib/client.js b/lib/client.js index dfff6fc8c..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"); @@ -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,25 +306,14 @@ class Client extends events.EventEmitter { /** * Wrapper for executing queries by rust driver - * @param {string | list} query - * @param {Array} params + * @param {string | PreparedInfo} query + * @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 @@ -348,26 +325,24 @@ 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); + } + if (withNamedParameters) { + params = utils.adaptNamedParamsPrepared(params, prepared); } - /** - * @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, ); @@ -511,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 @@ -554,26 +518,25 @@ 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]; + if (withNamedParameters) { + params = utils.adaptNamedParamsPrepared(params, prepared); + } - 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 +712,18 @@ 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; + + if (params && !Array.isArray(params)) { + params = utils.adaptNamedParamsPrepared(params, prepared); + } } else { types = hints[i] || []; } @@ -850,6 +820,7 @@ class Client extends events.EventEmitter { * @returns {void} */ } + /** * Callback used by execution methods. * @callback ResultCallback 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/new-utils.js b/lib/new-utils.js index dc282d0a5..54cdb52c1 100644 --- a/lib/new-utils.js +++ b/lib/new-utils.js @@ -137,10 +137,22 @@ 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 new file mode 100644 index 000000000..542bcaae8 --- /dev/null +++ b/lib/prepared.js @@ -0,0 +1,29 @@ +const _rust = require("../index"); +const { convertComplexType } = require("./types/cql-utils"); + +class PreparedInfo { + /** + * @param {Array} types + * @param {string} query + * @param {Array} colNames + */ + constructor(types, query, colNames) { + this.types = types; + this.query = query; + this.colNames = colNames; + } + + /** + * @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[0])); + let colNames = expectedTypes.map((t) => t[1].toLowerCase()); + return new PreparedInfo(types, query, colNames); + } +} + +module.exports.PreparedInfo = PreparedInfo; 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({