From e6b2137bf8bc1c7975cb73aae125ef0fe7276163 Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Mon, 24 Feb 2025 09:17:24 +0100 Subject: [PATCH 01/52] Move CharCounter from LocalTime to Utils --- src/types/local_time.rs | 23 +---------------------- src/utils.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/types/local_time.rs b/src/types/local_time.rs index b7610823..49084dc4 100644 --- a/src/types/local_time.rs +++ b/src/types/local_time.rs @@ -1,4 +1,4 @@ -use crate::utils::{bigint_to_i64, js_error}; +use crate::utils::{bigint_to_i64, js_error, CharCounter}; use napi::bindgen_prelude::BigInt; use scylla::frame::value::CqlTime; use std::fmt::{self, Write}; @@ -124,24 +124,3 @@ impl fmt::Display for LocalTimeWrapper { Ok(()) } } - -struct CharCounter { - count: usize, -} - -impl CharCounter { - fn new() -> Self { - CharCounter { count: 0 } - } - - fn count(self) -> usize { - self.count - } -} - -impl fmt::Write for CharCounter { - fn write_str(&mut self, s: &str) -> fmt::Result { - self.count = s.len(); - Ok(()) - } -} diff --git a/src/utils.rs b/src/utils.rs index 641a0fa9..b503dc9b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -73,3 +73,25 @@ pub(crate) fn bigint_to_i64(value: BigInt, error_msg: impl Display) -> napi::Res } Ok(value.words[0] as i64 * if value.sign_bit { -1 } else { 1 }) } + +#[derive(Default)] +pub struct CharCounter { + count: usize, +} + +impl CharCounter { + pub fn new() -> Self { + CharCounter { count: 0 } + } + + pub fn count(self) -> usize { + self.count + } +} + +impl fmt::Write for CharCounter { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.count = s.len(); + Ok(()) + } +} From 5235b32638f78afa05489dd5a36e60e829f4dc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 25 Feb 2025 12:01:04 +0100 Subject: [PATCH 02/52] Convert Row to class --- lib/types/row.js | 110 ++++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/lib/types/row.js b/lib/types/row.js index f351e17e..9b7eff69 100644 --- a/lib/types/row.js +++ b/lib/types/row.js @@ -5,65 +5,67 @@ * @param {Array} columns * @constructor */ -function Row(columns) { - if (!columns) { - throw new Error("Columns not defined"); +class Row { + constructor(columns) { + if (!columns) { + throw new Error("Columns not defined"); + } + // Private non-enumerable properties, with double underscore to avoid interfering with column names + Object.defineProperty(this, "__columns", { + value: columns, + enumerable: false, + writable: false, + }); + } + /** + * Returns the cell value. + * @param {String|Number} columnName Name or index of the column + */ + get(columnName) { + if (typeof columnName === "number") { + // its an index + return this[this.__columns[columnName].name]; + } + return this[columnName]; + } + /** + * Returns an array of the values of the row + * @returns {Array} + */ + values() { + const valuesArray = []; + this.forEach(function (val) { + valuesArray.push(val); + }); + return valuesArray; + } + /** + * Returns an array of the column names of the row + * @returns {Array} + */ + keys() { + const keysArray = []; + this.forEach(function (val, key) { + keysArray.push(key); + }); + return keysArray; + } + /** + * Executes the callback for each field in the row, containing the value as first parameter followed by the columnName + * @param {Function} callback + */ + forEach(callback) { + for (const columnName in this) { + if (!this.hasOwnProperty(columnName)) { + continue; + } + callback(this[columnName], columnName); + } } - // Private non-enumerable properties, with double underscore to avoid interfering with column names - Object.defineProperty(this, "__columns", { - value: columns, - enumerable: false, - writable: false, - }); } -/** - * Returns the cell value. - * @param {String|Number} columnName Name or index of the column - */ -Row.prototype.get = function (columnName) { - if (typeof columnName === "number") { - // its an index - return this[this.__columns[columnName].name]; - } - return this[columnName]; -}; -/** - * Returns an array of the values of the row - * @returns {Array} - */ -Row.prototype.values = function () { - const valuesArray = []; - this.forEach(function (val) { - valuesArray.push(val); - }); - return valuesArray; -}; -/** - * Returns an array of the column names of the row - * @returns {Array} - */ -Row.prototype.keys = function () { - const keysArray = []; - this.forEach(function (val, key) { - keysArray.push(key); - }); - return keysArray; -}; -/** - * Executes the callback for each field in the row, containing the value as first parameter followed by the columnName - * @param {Function} callback - */ -Row.prototype.forEach = function (callback) { - for (const columnName in this) { - if (!this.hasOwnProperty(columnName)) { - continue; - } - callback(this[columnName], columnName); - } -}; module.exports = Row; From 858c1900274c1e0e5fa51698b906c578c2c03596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 25 Feb 2025 09:36:17 +0100 Subject: [PATCH 03/52] Crete result as a Row Create a row of a result as a Row class instead of plain js object, to keep parity with datastax driver. Extend interface of Row constructor, so it can be created either from array of strings or array of objects with property name representing column name (the second options was present in Datastax driver). --- lib/types/results-wrapper.js | 3 +- lib/types/row.js | 18 ++-- test/unit-not-supported/basic-tests.js | 102 --------------------- test/unit/basic-tests.js | 120 +++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 110 deletions(-) create mode 100644 test/unit/basic-tests.js diff --git a/lib/types/results-wrapper.js b/lib/types/results-wrapper.js index 1f4e51cf..4052f172 100644 --- a/lib/types/results-wrapper.js +++ b/lib/types/results-wrapper.js @@ -7,6 +7,7 @@ const Duration = require("./duration"); const LocalTime = require("./local-time"); const InetAddress = require("./inet-address"); const { bigintToLong } = require("../new-utils"); +const Row = require("./row"); /** * Checks the type of value wrapper object and gets it from the underlying value @@ -97,7 +98,7 @@ function getRowsFromResultsWrapper(result) { for (let i = 0; i < rustRows.length; i++) { let cols = rustRows[i].getColumns(); - let collectedRow = {}; + let collectedRow = new Row(colNames); for (let j = 0; j < cols.length; j++) { // By default driver returns row as a map: // column name -> column value diff --git a/lib/types/row.js b/lib/types/row.js index 9b7eff69..54f4428f 100644 --- a/lib/types/row.js +++ b/lib/types/row.js @@ -1,11 +1,15 @@ "use strict"; /** @module types */ + /** * Represents a result row - * @param {Array} columns - * @constructor */ class Row { + /** + * Creates Row from array of column names. Column names can be provided either as array of strings + * or array of objects with property name representing column name. Any other metadata will be ignored + * @param {Array} columns + */ constructor(columns) { if (!columns) { throw new Error("Columns not defined"); @@ -24,7 +28,11 @@ class Row { get(columnName) { if (typeof columnName === "number") { // its an index - return this[this.__columns[columnName].name]; + columnName = this.__columns[columnName]; + if (typeof columnName.name == "string") { + columnName = columnName.name; + } + return this[columnName]; } return this[columnName]; } @@ -64,8 +72,4 @@ class Row { } } - - - - module.exports = Row; diff --git a/test/unit-not-supported/basic-tests.js b/test/unit-not-supported/basic-tests.js index efc220e0..3e0ea44d 100644 --- a/test/unit-not-supported/basic-tests.js +++ b/test/unit-not-supported/basic-tests.js @@ -375,108 +375,6 @@ describe("types", function () { }); }); }); - describe("Row", function () { - it("should get the value by column name or index", function () { - const columns = [ - { name: "first", type: { code: dataTypes.varchar } }, - { name: "second", type: { code: dataTypes.varchar } }, - ]; - const row = new types.Row(columns); - row["first"] = "hello"; - row["second"] = "world"; - assert.ok(row.get, "It should contain a get method"); - assert.strictEqual(row["first"], "hello"); - assert.strictEqual(row.get("first"), row["first"]); - assert.strictEqual(row.get(0), row["first"]); - assert.strictEqual(row.get("second"), row["second"]); - assert.strictEqual(row.get(1), row["second"]); - }); - it("should enumerate only columns defined", function () { - const columns = [ - { name: "col1", type: { code: dataTypes.varchar } }, - { name: "col2", type: { code: dataTypes.varchar } }, - ]; - const row = new types.Row(columns); - row["col1"] = "val1"; - row["col2"] = "val2"; - assert.strictEqual( - JSON.stringify(row), - JSON.stringify({ col1: "val1", col2: "val2" }), - ); - }); - it("should be serializable to json", function () { - let i; - let columns = [ - { name: "col1", type: { code: dataTypes.varchar } }, - { name: "col2", type: { code: dataTypes.varchar } }, - ]; - let row = new types.Row(columns, [ - utils.allocBufferFromString("val1"), - utils.allocBufferFromString("val2"), - ]); - row["col1"] = "val1"; - row["col2"] = "val2"; - assert.strictEqual( - JSON.stringify(row), - JSON.stringify({ col1: "val1", col2: "val2" }), - ); - - columns = [ - { name: "cid", type: { code: dataTypes.uuid } }, - { name: "ctid", type: { code: dataTypes.timeuuid } }, - { name: "clong", type: { code: dataTypes.bigint } }, - { name: "cvarint", type: { code: dataTypes.varint } }, - ]; - let rowValues = [ - types.Uuid.random(), - types.TimeUuid.now(), - types.Long.fromNumber(1000), - types.Integer.fromNumber(22), - ]; - row = new types.Row(columns); - for (i = 0; i < columns.length; i++) { - row[columns[i].name] = rowValues[i]; - } - let expected = util.format( - '{"cid":"%s","ctid":"%s","clong":"1000","cvarint":"22"}', - rowValues[0].toString(), - rowValues[1].toString(), - ); - assert.strictEqual(JSON.stringify(row), expected); - rowValues = [ - types.BigDecimal.fromString("1.762"), - new types.InetAddress( - utils.allocBufferFromArray([192, 168, 0, 1]), - ), - null, - ]; - columns = [ - { name: "cdecimal", type: { code: dataTypes.decimal } }, - { name: "inet1", type: { code: dataTypes.inet } }, - { name: "inet2", type: { code: dataTypes.inet } }, - ]; - row = new types.Row(columns); - for (i = 0; i < columns.length; i++) { - row[columns[i].name] = rowValues[i]; - } - expected = - '{"cdecimal":"1.762","inet1":"192.168.0.1","inet2":null}'; - assert.strictEqual(JSON.stringify(row), expected); - }); - it("should have values that can be inspected", function () { - const columns = [ - { name: "col10", type: { code: dataTypes.varchar } }, - { name: "col2", type: { code: dataTypes.int } }, - ]; - const row = new types.Row(columns); - row["col10"] = "val1"; - row["col2"] = 2; - helper.assertContains( - util.inspect(row), - util.inspect({ col10: "val1", col2: 2 }), - ); - }); - }); describe("uuid() backward-compatibility", function () { it("should generate a random string uuid", function () { const uuidRegex = diff --git a/test/unit/basic-tests.js b/test/unit/basic-tests.js new file mode 100644 index 00000000..969f5865 --- /dev/null +++ b/test/unit/basic-tests.js @@ -0,0 +1,120 @@ +"use strict"; +const { assert } = require("chai"); +const util = require("util"); + +const types = require("../../lib/types"); +const { dataTypes } = types; +const utils = require("../../lib/utils"); +const helper = require("../test-helper"); + +describe("Row", function () { + it("should get the value by column name or index", function () { + const columns = [ + { name: "first", type: { code: dataTypes.varchar } }, + { name: "second", type: { code: dataTypes.varchar } }, + ]; + const row = new types.Row(columns); + row["first"] = "hello"; + row["second"] = "world"; + assert.ok(row.get, "It should contain a get method"); + assert.strictEqual(row["first"], "hello"); + assert.strictEqual(row.get("first"), row["first"]); + assert.strictEqual(row.get(0), row["first"]); + assert.strictEqual(row.get("second"), row["second"]); + assert.strictEqual(row.get(1), row["second"]); + }); + it("should get the value by column name or index with new interface", function () { + const columns = ["first", "second"]; + const row = new types.Row(columns); + row["first"] = "hello"; + row["second"] = "world"; + assert.ok(row.get, "It should contain a get method"); + assert.strictEqual(row["first"], "hello"); + assert.strictEqual(row.get("first"), row["first"]); + assert.strictEqual(row.get(0), row["first"]); + assert.strictEqual(row.get("second"), row["second"]); + assert.strictEqual(row.get(1), row["second"]); + }); + it("should enumerate only columns defined", function () { + const columns = [ + { name: "col1", type: { code: dataTypes.varchar } }, + { name: "col2", type: { code: dataTypes.varchar } }, + ]; + const row = new types.Row(columns); + row["col1"] = "val1"; + row["col2"] = "val2"; + assert.strictEqual( + JSON.stringify(row), + JSON.stringify({ col1: "val1", col2: "val2" }), + ); + }); + it("should be serializable to json", function () { + let i; + let columns = [ + { name: "col1", type: { code: dataTypes.varchar } }, + { name: "col2", type: { code: dataTypes.varchar } }, + ]; + let row = new types.Row(columns, [ + utils.allocBufferFromString("val1"), + utils.allocBufferFromString("val2"), + ]); + row["col1"] = "val1"; + row["col2"] = "val2"; + assert.strictEqual( + JSON.stringify(row), + JSON.stringify({ col1: "val1", col2: "val2" }), + ); + + columns = [ + { name: "cid", type: { code: dataTypes.uuid } }, + { name: "ctid", type: { code: dataTypes.timeuuid } }, + { name: "clong", type: { code: dataTypes.bigint } }, + { name: "cvarint", type: { code: dataTypes.varint } }, + ]; + let rowValues = [ + types.Uuid.random(), + types.TimeUuid.now(), + types.Long.fromNumber(1000), + types.Integer.fromNumber(22), + ]; + row = new types.Row(columns); + for (i = 0; i < columns.length; i++) { + row[columns[i].name] = rowValues[i]; + } + let expected = util.format( + '{"cid":"%s","ctid":"%s","clong":"1000","cvarint":"22"}', + rowValues[0].toString(), + rowValues[1].toString(), + ); + assert.strictEqual(JSON.stringify(row), expected); + rowValues = [ + types.BigDecimal.fromString("1.762"), + new types.InetAddress(utils.allocBufferFromArray([192, 168, 0, 1])), + null, + ]; + columns = [ + { name: "cdecimal", type: { code: dataTypes.decimal } }, + { name: "inet1", type: { code: dataTypes.inet } }, + { name: "inet2", type: { code: dataTypes.inet } }, + ]; + row = new types.Row(columns); + for (i = 0; i < columns.length; i++) { + row[columns[i].name] = rowValues[i]; + } + expected = '{"cdecimal":"1.762","inet1":"192.168.0.1","inet2":null}'; + assert.strictEqual(JSON.stringify(row), expected); + }); + it("should have values that can be inspected", function () { + const columns = [ + { name: "col10", type: { code: dataTypes.varchar } }, + { name: "col2", type: { code: dataTypes.int } }, + ]; + const row = new types.Row(columns); + row["col10"] = "val1"; + row["col2"] = 2; + helper.assertContains( + util.inspect(row), + util.inspect({ col10: "val1", col2: 2 }), + ); + }); +}); From de3d71c31cb9bbba25ca46b725f80aa1333ae63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 25 Feb 2025 11:04:12 +0100 Subject: [PATCH 04/52] Enable 24 integration tests Enables tests for executing prepared queries. Test related to error throwing check only if error is thrown, instead of checking if error is of expected type. --- .../client-execute-prepared-tests.js | 131 ++++++++++++------ .../supported/client-execute-tests.js | 1 + 2 files changed, 93 insertions(+), 39 deletions(-) rename test/integration/{broken => supported}/client-execute-prepared-tests.js (96%) diff --git a/test/integration/broken/client-execute-prepared-tests.js b/test/integration/supported/client-execute-prepared-tests.js similarity index 96% rename from test/integration/broken/client-execute-prepared-tests.js rename to test/integration/supported/client-execute-prepared-tests.js index 463043b8..ad5e90a2 100644 --- a/test/integration/broken/client-execute-prepared-tests.js +++ b/test/integration/supported/client-execute-prepared-tests.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ "use strict"; const assert = require("assert"); const util = require("util"); @@ -35,7 +36,9 @@ describe("Client @SERVER_API", function () { ccmOptions: { yaml }, }); - it("should execute a prepared query with parameters on all hosts", function (done) { + // No support for client hosts + // TODO: fix this test + /* it("should execute a prepared query with parameters on all hosts", function (done) { const client = setupInfo.client; const query = util.format( "SELECT * FROM %s WHERE id1 = ?", @@ -59,17 +62,20 @@ describe("Client @SERVER_API", function () { }, done, ); - }); + }); */ it("should callback with error when query is invalid", function (done) { const client = setupInfo.client; const query = "SELECT WILL FAIL"; client.execute(query, ["system"], { prepare: 1 }, function (err) { assert.ok(err); - assert.strictEqual( + // Would require correct Error throwing + // TODO: fix this test + helper.assertInstanceOf(err, Error); + /* assert.strictEqual( err.code, types.responseErrorCodes.syntaxError, ); - assert.strictEqual(err.query, query); + assert.strictEqual(err.query, query); */ done(); }); }); @@ -117,7 +123,10 @@ describe("Client @SERVER_API", function () { params, { prepare: true }, (err) => { - helper.assertInstanceOf(err, TypeError); + // Would require correct error throwing + // TODO: fix this test + helper.assertInstanceOf(err, Error); + // helper.assertInstanceOf(err, TypeError); next(); }, ), @@ -137,7 +146,10 @@ describe("Client @SERVER_API", function () { params, { prepare: true }, (err) => { - helper.assertInstanceOf(err, TypeError); + // Would require correct error throwing + // TODO: fix this test + helper.assertInstanceOf(err, Error); + // helper.assertInstanceOf(err, TypeError); next(); }, ), @@ -237,14 +249,16 @@ describe("Client @SERVER_API", function () { { prepare: 1 }, function (err) { helper.assertInstanceOf(err, Error); - helper.assertInstanceOf( + // Would require correct Error throwing + // TODO: fix this test + /* helper.assertInstanceOf( err, errors.ResponseError, ); assert.strictEqual( err.code, types.responseErrorCodes.invalid, - ); + ); */ next(); }, ); @@ -262,14 +276,16 @@ describe("Client @SERVER_API", function () { { prepare: 1 }, function (err) { helper.assertInstanceOf(err, Error); - helper.assertInstanceOf( + // Would require correct Error throwing + // TODO: fix this test + /* helper.assertInstanceOf( err, errors.ResponseError, ); assert.strictEqual( err.code, types.responseErrorCodes.invalid, - ); + ); */ next(); }, ); @@ -281,7 +297,9 @@ describe("Client @SERVER_API", function () { done, ); }); - context("when prepareOnAllHosts set to false", function () { + // No support for prepareOnAllHosts option + // TODO: fix this test + /* context("when prepareOnAllHosts set to false", function () { it("should execute a prepared query on all hosts", function (done) { const client = newInstance({ prepareOnAllHosts: false }); utils.timesSeries( @@ -315,7 +333,7 @@ describe("Client @SERVER_API", function () { helper.finish(client, done), ); }); - }); + }); */ it("should fail if the type does not match", function (done) { const client = setupInfo.client; client.execute( @@ -329,7 +347,9 @@ describe("Client @SERVER_API", function () { }, ); }); - it("should serialize all guessed types", function (done) { + // Likely failing due to missing support for used type + // TODO: identify the problem and fix this test + /* it("should serialize all guessed types", function (done) { const values = [ types.Uuid.random(), "as", @@ -349,7 +369,7 @@ describe("Client @SERVER_API", function () { "id, ascii_sample, text_sample, int_sample, bigint_sample, double_sample, blob_sample, " + "boolean_sample, timestamp_sample, inet_sample, timeuuid_sample, list_sample, set_sample"; serializationTest(setupInfo.client, values, columnNames, done); - }); + }); */ it("should serialize all null values", function (done) { const values = [ types.Uuid.random(), @@ -402,7 +422,9 @@ describe("Client @SERVER_API", function () { }, ); }); - it("should encode and decode varint values", function (done) { + // Requires better support for encoding: see #142 + // TODO: fix this test + /* it("should encode and decode varint values", function (done) { const client = setupInfo.client; const table = commonKs + "." + helper.getRandomName("table"); const expectedRows = {}; @@ -549,8 +571,11 @@ describe("Client @SERVER_API", function () { ], done, ); - }); - describe("with named parameters", function () { + }); */ + + // No support for named parameters + // TODO: fix this test + /* 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", @@ -633,8 +658,11 @@ describe("Client @SERVER_API", function () { ); }, ); - }); - it("should encode and decode maps using Map polyfills", function (done) { + }); */ + + // No support for polyfills + // TODO: fix this test + /* it("should encode and decode maps using Map polyfills", function (done) { const client = newInstance({ encoding: { map: helper.Map } }); const table = commonKs + "." + helper.getRandomName("table"); const MapPF = helper.Map; @@ -869,7 +897,11 @@ describe("Client @SERVER_API", function () { done, ); }); - vit("2.1", "should support protocol level timestamp", function (done) { + */ + + // Protocol level timestamp implemented in #92 + // TODO: fix this test + /* vit("2.1", "should support protocol level timestamp", function (done) { const client = setupInfo.client; const id = Uuid.random(); const timestamp = types.generateTimestamp(new Date(), 456); @@ -919,8 +951,11 @@ describe("Client @SERVER_API", function () { ], done, ); - }); - vit("2.1.3", "should support nested collections", function (done) { + }); */ + + // No support for client keyspace option + // TODO: fix this test + /* vit("2.1.3", "should support nested collections", function (done) { const client = newInstance({ keyspace: commonKs, queryOptions: { @@ -998,8 +1033,11 @@ describe("Client @SERVER_API", function () { ], done, ); - }); - vit( + }); */ + + // No support for warnings in result set + // TODO: fix this test + /* vit( "2.2", "should include the warning in the ResultSet", function (done) { @@ -1043,7 +1081,7 @@ describe("Client @SERVER_API", function () { }, ); }, - ); + ); */ it("should support hardcoded parameters that are part of the routing key", function (done) { const client = setupInfo.client; const table = helper.getRandomName("tbl"); @@ -1294,11 +1332,14 @@ describe("Client @SERVER_API", function () { function validateResponseError(callback) { return (err) => { - helper.assertInstanceOf(err, errors.ResponseError); + // Would require full error throwing + // TODO: fix this test + helper.assertInstanceOf(err, Error); + /* helper.assertInstanceOf(err, errors.ResponseError); assert.strictEqual( err.code, types.responseErrorCodes.invalid, - ); + ); */ callback(); }; } @@ -1331,7 +1372,9 @@ describe("Client @SERVER_API", function () { ); }); - describe("with a different keyspace", function () { + // No support for client keyspace option + // TODO: fix this test + /* describe("with a different keyspace", function () { it("should fill in the keyspace in the query options passed to the lbp", () => { const lbp = new loadBalancing.RoundRobinPolicy(); lbp.newQueryPlanOriginal = lbp.newQueryPlan; @@ -1364,9 +1407,11 @@ describe("Client @SERVER_API", function () { assert.strictEqual(options.getKeyspace(), commonKs); }); }); - }); + }); */ - describe("with udt and tuple", function () { + // No support for udt and tuples + // TODO: fix this test + /* describe("with udt and tuple", function () { before(function (done) { const client = setupInfo.client; utils.series( @@ -1612,7 +1657,7 @@ describe("Client @SERVER_API", function () { }); }, ); - }); + }); */ describe("with smallint and tinyint types", function () { const insertQuery = @@ -1678,7 +1723,9 @@ describe("Client @SERVER_API", function () { }, ); }); - describe("with date and time types", function () { + // Would require better encoding, see #142 + // TODO: fix this test + /* describe("with date and time types", function () { const LocalDate = types.LocalDate; const LocalTime = types.LocalTime; const insertQuery = @@ -1776,7 +1823,7 @@ describe("Client @SERVER_API", function () { ); }, ); - }); + }); */ describe("with unset", function () { vit("2.2", "should allow unset as a valid value", function (done) { const client1 = newInstance(); @@ -2363,10 +2410,14 @@ describe("Client @SERVER_API", function () { }); */ }); - numericTests(commonKs, true); - pagingTests(commonKs, true); + // No support for paging + // TODO: fix this test + /* numericTests(commonKs, true); + pagingTests(commonKs, true); */ - it("should not use keyspace if set on options for lower protocol versions", function () { + // No support for client keyspace options + // TODO: fix this test + /* it("should not use keyspace if set on options for lower protocol versions", function () { if (helper.isDseGreaterThan("6.0")) { return this.skip(); } @@ -2383,8 +2434,10 @@ describe("Client @SERVER_API", function () { .catch(function (err) { helper.assertInstanceOf(err, errors.ResponseError); }); - }); - describe("With schema changes made while querying", () => { + }); */ + // No support for used client and query options + // TODO: fix this test + /* describe("With schema changes made while querying", () => { // Note: Since the driver does not make use of result metadata on prepared statement // it should inheritently be resilient to schema changes since it uses the metadata // in the rows responses. However, if NODEJS-433 is implemented the driver will @@ -2576,7 +2629,7 @@ describe("Client @SERVER_API", function () { done, ); }); - }); + }); */ }); }); diff --git a/test/integration/supported/client-execute-tests.js b/test/integration/supported/client-execute-tests.js index 719bb0bf..1f7d969f 100644 --- a/test/integration/supported/client-execute-tests.js +++ b/test/integration/supported/client-execute-tests.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ "use strict"; const assert = require("assert"); const util = require("util"); From aa3de6b031750416449a423b03892593e4069c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Mon, 9 Dec 2024 15:25:03 +0100 Subject: [PATCH 05/52] Utils refactor Minor changes to code style: - Update hashset to class - Remove deprecated uses of Buffer() - Update string formatting to new format --- lib/utils.js | 99 +++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 09eaae63..93b5ecae 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -51,7 +51,7 @@ const allocBufferFromArray = allocBufferFromArrayDeprecated; function allocBufferDeprecated(size) { - return new Buffer(size); + return new Buffer.from(size); } function allocBufferFillDeprecated(size) { @@ -65,7 +65,7 @@ function allocBufferFromStringDeprecated(text, encoding) { throw new TypeError("Expected string, obtained " + util.inspect(text)); } - return new Buffer(text, encoding); + return new Buffer.from(text, encoding); } function allocBufferFromArrayDeprecated(arr) { @@ -73,7 +73,7 @@ function allocBufferFromArrayDeprecated(arr) { throw new TypeError("Expected Array, obtained " + util.inspect(arr)); } - return new Buffer(arr); + return new Buffer.from(arr); } /** @@ -330,7 +330,7 @@ function insertSorted(arr, item, compareFunc) { function validateFn(fn, name) { if (typeof fn !== "function") { throw new errors.ArgumentError( - util.format("%s is not a function", name || "callback"), + `${name || "callback"} is not a function`, ); } return fn; @@ -357,9 +357,7 @@ function adaptNamedParamsPrepared(params, columns) { const name = columns[i].name; if (!params.hasOwnProperty(name)) { - throw new errors.ArgumentError( - util.format('Parameter "%s" not defined', name), - ); + throw new errors.ArgumentError(`Parameter "${name}" not defined`); } paramsArray[i] = params[name]; keys[name] = i; @@ -509,54 +507,51 @@ function shuffleArray(arr) { /** * Represents a unique set of values. - * @constructor - */ -function HashSet() { - this.length = 0; - this.items = {}; -} - -/** - * Adds a new item to the set. - * @param {Object} key - * @returns {boolean} Returns true if it was added to the set; false if the key is already present. */ -HashSet.prototype.add = function (key) { - if (this.contains(key)) { - return false; +class HashSet { + constructor() { + this.length = 0; + this.items = {}; } - this.items[key] = true; - this.length++; - return true; -}; - -/** - * @returns {boolean} Returns true if the key is present in the set. - */ -HashSet.prototype.contains = function (key) { - return this.length > 0 && this.items[key] === true; -}; - -/** - * Removes the item from set. - * @param key - * @return {boolean} Returns true if the key existed and was removed, otherwise it returns false. - */ -HashSet.prototype.remove = function (key) { - if (!this.contains(key)) { - return false; + /** + * Adds a new item to the set. + * @param {Object} key + * @returns {boolean} Returns true if it was added to the set; false if the key is already present. + */ + add(key) { + if (this.contains(key)) { + return false; + } + this.items[key] = true; + this.length++; + return true; } - delete this.items[key]; - this.length--; -}; - -/** - * Returns an array containing the set items. - * @returns {Array} - */ -HashSet.prototype.toArray = function () { - return Object.keys(this.items); -}; + /** + * @returns {boolean} Returns true if the key is present in the set. + */ + contains(key) { + return this.length > 0 && this.items[key] === true; + } + /** + * Removes the item from set. + * @param key + * @return {boolean} Returns true if the key existed and was removed, otherwise it returns false. + */ + remove(key) { + if (!this.contains(key)) { + return false; + } + delete this.items[key]; + this.length--; + } + /** + * Returns an array containing the set items. + * @returns {Array} + */ + toArray() { + return Object.keys(this.items); + } +} /** * Utility class that resolves host names into addresses. From dcdeff129608c723ebdacbeb1ccb581d2f3515f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Mon, 9 Dec 2024 14:37:32 +0100 Subject: [PATCH 06/52] Convert integer to class syntax --- lib/types/integer.js | 1356 ++++++++++++++++++++---------------------- 1 file changed, 661 insertions(+), 695 deletions(-) diff --git a/lib/types/integer.js b/lib/types/integer.js index d40da681..cb8f2a6a 100644 --- a/lib/types/integer.js +++ b/lib/types/integer.js @@ -3,13 +3,6 @@ const utils = require("../utils"); /** - * Constructs a two's-complement integer an array containing bits of the - * integer in 32-bit (signed) pieces, given in little-endian order (i.e., - * lowest-order bits in the first piece), and the sign of -1 or 0. - * - * See the from* functions below for other convenient ways of constructing - * Integers. - * * The internal representation of an integer is an array of 32-bit signed * pieces, along with a sign (0 or -1) that indicates the contents of all the * other 32-bit pieces out to infinity. We use 32-bit pieces because these are @@ -18,779 +11,752 @@ const utils = require("../utils"); * pieces, which can easily be multiplied within Javascript's floating-point * representation without overflow or change in sign. * - * @constructor - * @param {Array.} bits Array containing the bits of the number. - * @param {number} sign The sign of the number: -1 for negative and 0 positive. * @final */ -function Integer(bits, sign) { +class Integer { /** - * @type {!Array.} - * @private + * Constructs a two's-complement integer an array containing bits of the + * integer in 32-bit (signed) pieces, given in little-endian order (i.e., + * lowest-order bits in the first piece), and the sign of -1 or 0. + * + * See the from* functions below for other convenient ways of constructing + * Integers. + * @param {Array.} bits Array containing the bits of the number. + * @param {number} sign The sign of the number: -1 for negative and 0 positive. */ - this.bits_ = []; - + constructor(bits, sign) { + /** + * @type {!Array.} + * @private + */ + this.bits_ = []; + + /** + * @type {number} + * @private + */ + this.sign_ = sign; + + // Copy the 32-bit signed integer values passed in. We prune out those at the + // top that equal the sign since they are redundant. + let top = true; + for (let i = bits.length - 1; i >= 0; i--) { + let val = bits[i] | 0; + if (!top || val != sign) { + this.bits_[i] = val; + top = false; + } + } + } /** - * @type {number} - * @private + * Returns an Integer representing the given (32-bit) integer value. + * @param {number} value A 32-bit integer value. + * @return {!Integer} The corresponding Integer value. */ - this.sign_ = sign; - - // Copy the 32-bit signed integer values passed in. We prune out those at the - // top that equal the sign since they are redundant. - let top = true; - for (let i = bits.length - 1; i >= 0; i--) { - let val = bits[i] | 0; - if (!top || val != sign) { - this.bits_[i] = val; - top = false; + static fromInt(value) { + if (-128 <= value && value < 128) { + let cachedObj = Integer.IntCache_[value]; + if (cachedObj) { + return cachedObj; + } } - } -} - -// NOTE: Common constant values ZERO, ONE, NEG_ONE, etc. are defined below the -// from* methods on which they depend. - -/** - * A cache of the Integer representations of small integer values. - * @type {!Object} - * @private - */ -Integer.IntCache_ = {}; -/** - * Returns an Integer representing the given (32-bit) integer value. - * @param {number} value A 32-bit integer value. - * @return {!Integer} The corresponding Integer value. - */ -Integer.fromInt = function (value) { - if (-128 <= value && value < 128) { - let cachedObj = Integer.IntCache_[value]; - if (cachedObj) { - return cachedObj; + let obj = new Integer([value | 0], value < 0 ? -1 : 0); + if (-128 <= value && value < 128) { + Integer.IntCache_[value] = obj; } + return obj; } - - let obj = new Integer([value | 0], value < 0 ? -1 : 0); - if (-128 <= value && value < 128) { - Integer.IntCache_[value] = obj; - } - return obj; -}; - -/** - * Returns an Integer representing the given value, provided that it is a finite - * number. Otherwise, zero is returned. - * @param {number} value The value in question. - * @return {!Integer} The corresponding Integer value. - */ -Integer.fromNumber = function (value) { - if (isNaN(value) || !isFinite(value)) { - return Integer.ZERO; - } else if (value < 0) { - return Integer.fromNumber(-value).negate(); - } - let bits = []; - let pow = 1; - for (let i = 0; value >= pow; i++) { - bits[i] = (value / pow) | 0; - pow *= Integer.TWO_PWR_32_DBL_; - } - return new Integer(bits, 0); -}; - -/** - * Returns a Integer representing the value that comes by concatenating the - * given entries, each is assumed to be 32 signed bits, given in little-endian - * order (lowest order bits in the lowest index), and sign-extending the highest - * order 32-bit value. - * @param {Array.} bits The bits of the number, in 32-bit signed pieces, - * in little-endian order. - * @return {!Integer} The corresponding Integer value. - */ -Integer.fromBits = function (bits) { - let high = bits[bits.length - 1]; - // noinspection JSBitwiseOperatorUsage - return new Integer(bits, high & (1 << 31) ? -1 : 0); -}; - -/** - * Returns an Integer representation of the given string, written using the - * given radix. - * @param {string} str The textual representation of the Integer. - * @param {number=} optRadix The radix in which the text is written. - * @return {!Integer} The corresponding Integer value. - */ -Integer.fromString = function (str, optRadix) { - if (str.length == 0) { - throw TypeError("number format error: empty string"); + /** + * Returns an Integer representing the given value, provided that it is a finite + * number. Otherwise, zero is returned. + * @param {number} value The value in question. + * @return {!Integer} The corresponding Integer value. + */ + static fromNumber(value) { + if (isNaN(value) || !isFinite(value)) { + return Integer.ZERO; + } else if (value < 0) { + return Integer.fromNumber(-value).negate(); + } + let bits = []; + let pow = 1; + for (let i = 0; value >= pow; i++) { + bits[i] = (value / pow) | 0; + pow *= Integer.TWO_PWR_32_DBL_; + } + return new Integer(bits, 0); } - - let radix = optRadix || 10; - if (radix < 2 || 36 < radix) { - throw Error("radix out of range: " + radix); + /** + * Returns a Integer representing the value that comes by concatenating the + * given entries, each is assumed to be 32 signed bits, given in little-endian + * order (lowest order bits in the lowest index), and sign-extending the highest + * order 32-bit value. + * @param {Array.} bits The bits of the number, in 32-bit signed pieces, + * in little-endian order. + * @return {!Integer} The corresponding Integer value. + */ + static fromBits(bits) { + let high = bits[bits.length - 1]; + // noinspection JSBitwiseOperatorUsage + return new Integer(bits, high & (1 << 31) ? -1 : 0); } + /** + * Returns an Integer representation of the given string, written using the + * given radix. + * @param {string} str The textual representation of the Integer. + * @param {number=} optRadix The radix in which the text is written. + * @return {!Integer} The corresponding Integer value. + */ + static fromString(str, optRadix) { + if (str.length == 0) { + throw TypeError("number format error: empty string"); + } - if (str.charAt(0) == "-") { - return Integer.fromString(str.substring(1), radix).negate(); - } else if (str.indexOf("-") >= 0) { - throw TypeError('number format error: interior "-" character'); - } + let radix = optRadix || 10; + if (radix < 2 || 36 < radix) { + throw Error("radix out of range: " + radix); + } - // Do several (8) digits each time through the loop, so as to - // minimize the calls to the very expensive emulated div. - let radixToPower = Integer.fromNumber(Math.pow(radix, 8)); + if (str.charAt(0) == "-") { + return Integer.fromString(str.substring(1), radix).negate(); + } else if (str.indexOf("-") >= 0) { + throw TypeError('number format error: interior "-" character'); + } - let result = Integer.ZERO; - for (let i = 0; i < str.length; i += 8) { - let size = Math.min(8, str.length - i); - let value = parseInt(str.substring(i, i + size), radix); - if (size < 8) { - let power = Integer.fromNumber(Math.pow(radix, size)); - result = result.multiply(power).add(Integer.fromNumber(value)); - } else { - result = result.multiply(radixToPower); - result = result.add(Integer.fromNumber(value)); + // Do several (8) digits each time through the loop, so as to + // minimize the calls to the very expensive emulated div. + let radixToPower = Integer.fromNumber(Math.pow(radix, 8)); + + let result = Integer.ZERO; + for (let i = 0; i < str.length; i += 8) { + let size = Math.min(8, str.length - i); + let value = parseInt(str.substring(i, i + size), radix); + if (size < 8) { + let power = Integer.fromNumber(Math.pow(radix, size)); + result = result.multiply(power).add(Integer.fromNumber(value)); + } else { + result = result.multiply(radixToPower); + result = result.add(Integer.fromNumber(value)); + } } + return result; } - return result; -}; - -/** - * Returns an Integer representation of a given big endian Buffer. - * The internal representation of bits contains bytes in groups of 4 - * @param {Buffer} buf - * @returns {Integer} - */ -Integer.fromBuffer = function (buf) { - let bits = new Array(Math.ceil(buf.length / 4)); - // noinspection JSBitwiseOperatorUsage - let sign = buf[0] & (1 << 7) ? -1 : 0; - for (let i = 0; i < bits.length; i++) { - let offset = buf.length - (i + 1) * 4; - let value; - if (offset < 0) { - // The buffer length is not multiple of 4 - offset = offset + 4; - value = 0; - for (let j = 0; j < offset; j++) { - let byte = buf[j]; + /** + * Returns an Integer representation of a given big endian Buffer. + * The internal representation of bits contains bytes in groups of 4 + * @param {Buffer} buf + * @returns {Integer} + */ + static fromBuffer(buf) { + let bits = new Array(Math.ceil(buf.length / 4)); + // noinspection JSBitwiseOperatorUsage + let sign = buf[0] & (1 << 7) ? -1 : 0; + for (let i = 0; i < bits.length; i++) { + let offset = buf.length - (i + 1) * 4; + let value; + if (offset < 0) { + // The buffer length is not multiple of 4 + offset = offset + 4; + value = 0; + for (let j = 0; j < offset; j++) { + let byte = buf[j]; + if (sign === -1) { + // invert the bits + byte = ~byte & 0xff; + } + value = value | (byte << ((offset - j - 1) * 8)); + } if (sign === -1) { - // invert the bits - byte = ~byte & 0xff; + // invert all the bits + value = ~value; } - value = value | (byte << ((offset - j - 1) * 8)); + } else { + value = buf.readInt32BE(offset); } - if (sign === -1) { - // invert all the bits - value = ~value; - } - } else { - value = buf.readInt32BE(offset); + bits[i] = value; } - bits[i] = value; + return new Integer(bits, sign); } - return new Integer(bits, sign); -}; - -/** - * Returns a big endian buffer representation of an Integer. - * Internally the bits are represented using 4 bytes groups (numbers), - * in the Buffer representation there might be the case where we need less than the 4 bytes. - * For example: 0x00000001 -> '01', 0xFFFFFFFF -> 'FF', 0xFFFFFF01 -> 'FF01' - * @param {Integer} value - * @returns {Buffer} - */ -Integer.toBuffer = function (value) { - let sign = value.sign_; - let bits = value.bits_; - if (bits.length === 0) { - // [0] or [0xffffffff] - return utils.allocBufferFromArray([value.sign_]); - } - // the high bits might need to be represented in less than 4 bytes - let highBits = bits[bits.length - 1]; - if (sign === -1) { - highBits = ~highBits; - } - let high = []; - if (highBits >>> 24 > 0) { - high.push((highBits >> 24) & 0xff); - } - if (highBits >>> 16 > 0) { - high.push((highBits >> 16) & 0xff); - } - if (highBits >>> 8 > 0) { - high.push((highBits >> 8) & 0xff); - } - high.push(highBits & 0xff); - if (sign === -1) { - // The byte containing the sign bit got removed - if (high[0] >> 7 !== 0) { - // it is going to be negated + /** + * Returns a big endian buffer representation of an Integer. + * Internally the bits are represented using 4 bytes groups (numbers), + * in the Buffer representation there might be the case where we need less than the 4 bytes. + * For example: 0x00000001 -> '01', 0xFFFFFFFF -> 'FF', 0xFFFFFF01 -> 'FF01' + * @param {Integer} value + * @returns {Buffer} + */ + static toBuffer(value) { + let sign = value.sign_; + let bits = value.bits_; + if (bits.length === 0) { + // [0] or [0xffffffff] + return utils.allocBufferFromArray([value.sign_]); + } + // the high bits might need to be represented in less than 4 bytes + let highBits = bits[bits.length - 1]; + if (sign === -1) { + highBits = ~highBits; + } + let high = []; + if (highBits >>> 24 > 0) { + high.push((highBits >> 24) & 0xff); + } + if (highBits >>> 16 > 0) { + high.push((highBits >> 16) & 0xff); + } + if (highBits >>> 8 > 0) { + high.push((highBits >> 8) & 0xff); + } + high.push(highBits & 0xff); + if (sign === -1) { + // The byte containing the sign bit got removed + if (high[0] >> 7 !== 0) { + // it is going to be negated + high.unshift(0); + } + } else if (high[0] >> 7 !== 0) { + // its positive but it lost the byte containing the sign bit high.unshift(0); } - } else if (high[0] >> 7 !== 0) { - // its positive but it lost the byte containing the sign bit - high.unshift(0); + let buf = utils.allocBufferUnsafe(high.length + (bits.length - 1) * 4); + for (let j = 0; j < high.length; j++) { + let b = high[j]; + if (sign === -1) { + buf[j] = ~b; + } else { + buf[j] = b; + } + } + for (let i = 0; i < bits.length - 1; i++) { + let group = bits[bits.length - 2 - i]; + let offset = high.length + i * 4; + buf.writeInt32BE(group, offset); + } + return buf; } - let buf = utils.allocBufferUnsafe(high.length + (bits.length - 1) * 4); - for (let j = 0; j < high.length; j++) { - let b = high[j]; - if (sign === -1) { - buf[j] = ~b; - } else { - buf[j] = b; + /** + * Carries any overflow from the given index into later entries. + * @param {Array.} bits Array of 16-bit values in little-endian order. + * @param {number} index The index in question. + * @private + */ + static carry16_(bits, index) { + while ((bits[index] & 0xffff) != bits[index]) { + bits[index + 1] += bits[index] >>> 16; + bits[index] &= 0xffff; } } - for (let i = 0; i < bits.length - 1; i++) { - let group = bits[bits.length - 2 - i]; - let offset = high.length + i * 4; - buf.writeInt32BE(group, offset); + /** + * Returns the value, assuming it is a 32-bit integer. + * @return {number} The corresponding int value. + */ + toInt() { + return this.bits_.length > 0 ? this.bits_[0] : this.sign_; } - return buf; -}; - -/** - * A number used repeatedly in calculations. This must appear before the first - * call to the from* functions below. - * @type {number} - * @private - */ -Integer.TWO_PWR_32_DBL_ = (1 << 16) * (1 << 16); - -/** @type {!Integer} */ -Integer.ZERO = Integer.fromInt(0); + /** @return {number} The closest floating-point representation to this value. */ + toNumber() { + if (this.isNegative()) { + return -this.negate().toNumber(); + } + let val = 0; + let pow = 1; + for (let i = 0; i < this.bits_.length; i++) { + val += this.getBitsUnsigned(i) * pow; + pow *= Integer.TWO_PWR_32_DBL_; + } + return val; + } + /** + * @param {number=} optRadix The radix in which the text should be written. + * @return {string} The textual representation of this value. + * @override + */ + toString(optRadix) { + let radix = optRadix || 10; + if (radix < 2 || 36 < radix) { + throw Error("radix out of range: " + radix); + } -/** @type {!Integer} */ -Integer.ONE = Integer.fromInt(1); + if (this.isZero()) { + return "0"; + } else if (this.isNegative()) { + return "-" + this.negate().toString(radix); + } -/** - * @type {!Integer} - * @private - */ -Integer.TWO_PWR_24_ = Integer.fromInt(1 << 24); + // Do several (6) digits each time through the loop, so as to + // minimize the calls to the very expensive emulated div. + let radixToPower = Integer.fromNumber(Math.pow(radix, 6)); -/** - * Returns the value, assuming it is a 32-bit integer. - * @return {number} The corresponding int value. - */ -Integer.prototype.toInt = function () { - return this.bits_.length > 0 ? this.bits_[0] : this.sign_; -}; + let rem = this; + let result = ""; + while (true) { + let remDiv = rem.divide(radixToPower); + let intval = rem.subtract(remDiv.multiply(radixToPower)).toInt(); + let digits = intval.toString(radix); -/** @return {number} The closest floating-point representation to this value. */ -Integer.prototype.toNumber = function () { - if (this.isNegative()) { - return -this.negate().toNumber(); + rem = remDiv; + if (rem.isZero()) { + return digits + result; + } + while (digits.length < 6) { + digits = "0" + digits; + } + result = "" + digits + result; + } } - let val = 0; - let pow = 1; - for (let i = 0; i < this.bits_.length; i++) { - val += this.getBitsUnsigned(i) * pow; - pow *= Integer.TWO_PWR_32_DBL_; + /** + * Returns the index-th 32-bit (signed) piece of the Integer according to + * little-endian order (i.e., index 0 contains the smallest bits). + * @param {number} index The index in question. + * @return {number} The requested 32-bits as a signed number. + */ + getBits(index) { + if (index < 0) { + return 0; // Allowing this simplifies bit shifting operations below... + } else if (index < this.bits_.length) { + return this.bits_[index]; + } + return this.sign_; } - return val; -}; - -/** - * @param {number=} optRadix The radix in which the text should be written. - * @return {string} The textual representation of this value. - * @override - */ -Integer.prototype.toString = function (optRadix) { - let radix = optRadix || 10; - if (radix < 2 || 36 < radix) { - throw Error("radix out of range: " + radix); + /** + * Returns the index-th 32-bit piece as an unsigned number. + * @param {number} index The index in question. + * @return {number} The requested 32-bits as an unsigned number. + */ + getBitsUnsigned(index) { + let val = this.getBits(index); + return val >= 0 ? val : Integer.TWO_PWR_32_DBL_ + val; } - - if (this.isZero()) { - return "0"; - } else if (this.isNegative()) { - return "-" + this.negate().toString(radix); + /** @return {number} The sign bit of this number, -1 or 0. */ + getSign() { + return this.sign_; } - - // Do several (6) digits each time through the loop, so as to - // minimize the calls to the very expensive emulated div. - let radixToPower = Integer.fromNumber(Math.pow(radix, 6)); - - let rem = this; - let result = ""; - while (true) { - let remDiv = rem.divide(radixToPower); - let intval = rem.subtract(remDiv.multiply(radixToPower)).toInt(); - let digits = intval.toString(radix); - - rem = remDiv; - if (rem.isZero()) { - return digits + result; + /** @return {boolean} Whether this value is zero. */ + isZero() { + if (this.sign_ != 0) { + return false; } - while (digits.length < 6) { - digits = "0" + digits; + for (let i = 0; i < this.bits_.length; i++) { + if (this.bits_[i] != 0) { + return false; + } } - result = "" + digits + result; + return true; } -}; - -/** - * Returns the index-th 32-bit (signed) piece of the Integer according to - * little-endian order (i.e., index 0 contains the smallest bits). - * @param {number} index The index in question. - * @return {number} The requested 32-bits as a signed number. - */ -Integer.prototype.getBits = function (index) { - if (index < 0) { - return 0; // Allowing this simplifies bit shifting operations below... - } else if (index < this.bits_.length) { - return this.bits_[index]; + /** @return {boolean} Whether this value is negative. */ + isNegative() { + return this.sign_ == -1; } - return this.sign_; -}; - -/** - * Returns the index-th 32-bit piece as an unsigned number. - * @param {number} index The index in question. - * @return {number} The requested 32-bits as an unsigned number. - */ -Integer.prototype.getBitsUnsigned = function (index) { - let val = this.getBits(index); - return val >= 0 ? val : Integer.TWO_PWR_32_DBL_ + val; -}; - -/** @return {number} The sign bit of this number, -1 or 0. */ -Integer.prototype.getSign = function () { - return this.sign_; -}; - -/** @return {boolean} Whether this value is zero. */ -Integer.prototype.isZero = function () { - if (this.sign_ != 0) { - return false; - } - for (let i = 0; i < this.bits_.length; i++) { - if (this.bits_[i] != 0) { + /** @return {boolean} Whether this value is odd. */ + isOdd() { + return ( + (this.bits_.length == 0 && this.sign_ == -1) || + (this.bits_.length > 0 && (this.bits_[0] & 1) != 0) + ); + } + /** + * @param {Integer} other Integer to compare against. + * @return {boolean} Whether this Integer equals the other. + */ + equals(other) { + if (this.sign_ != other.sign_) { return false; } + let len = Math.max(this.bits_.length, other.bits_.length); + for (let i = 0; i < len; i++) { + if (this.getBits(i) != other.getBits(i)) { + return false; + } + } + return true; } - return true; -}; - -/** @return {boolean} Whether this value is negative. */ -Integer.prototype.isNegative = function () { - return this.sign_ == -1; -}; - -/** @return {boolean} Whether this value is odd. */ -Integer.prototype.isOdd = function () { - return ( - (this.bits_.length == 0 && this.sign_ == -1) || - (this.bits_.length > 0 && (this.bits_[0] & 1) != 0) - ); -}; - -/** - * @param {Integer} other Integer to compare against. - * @return {boolean} Whether this Integer equals the other. - */ -Integer.prototype.equals = function (other) { - if (this.sign_ != other.sign_) { - return false; + /** + * @param {Integer} other Integer to compare against. + * @return {boolean} Whether this Integer does not equal the other. + */ + notEquals(other) { + return !this.equals(other); } - let len = Math.max(this.bits_.length, other.bits_.length); - for (let i = 0; i < len; i++) { - if (this.getBits(i) != other.getBits(i)) { - return false; - } + /** + * @param {Integer} other Integer to compare against. + * @return {boolean} Whether this Integer is greater than the other. + */ + greaterThan(other) { + return this.compare(other) > 0; } - return true; -}; - -/** - * @param {Integer} other Integer to compare against. - * @return {boolean} Whether this Integer does not equal the other. - */ -Integer.prototype.notEquals = function (other) { - return !this.equals(other); -}; - -/** - * @param {Integer} other Integer to compare against. - * @return {boolean} Whether this Integer is greater than the other. - */ -Integer.prototype.greaterThan = function (other) { - return this.compare(other) > 0; -}; - -/** - * @param {Integer} other Integer to compare against. - * @return {boolean} Whether this Integer is greater than or equal to the other. - */ -Integer.prototype.greaterThanOrEqual = function (other) { - return this.compare(other) >= 0; -}; - -/** - * @param {Integer} other Integer to compare against. - * @return {boolean} Whether this Integer is less than the other. - */ -Integer.prototype.lessThan = function (other) { - return this.compare(other) < 0; -}; - -/** - * @param {Integer} other Integer to compare against. - * @return {boolean} Whether this Integer is less than or equal to the other. - */ -Integer.prototype.lessThanOrEqual = function (other) { - return this.compare(other) <= 0; -}; - -/** - * Compares this Integer with the given one. - * @param {Integer} other Integer to compare against. - * @return {number} 0 if they are the same, 1 if the this is greater, and -1 - * if the given one is greater. - */ -Integer.prototype.compare = function (other) { - let diff = this.subtract(other); - if (diff.isNegative()) { - return -1; - } else if (diff.isZero()) { - return 0; + /** + * @param {Integer} other Integer to compare against. + * @return {boolean} Whether this Integer is greater than or equal to the other. + */ + greaterThanOrEqual(other) { + return this.compare(other) >= 0; } - return +1; -}; - -/** - * Returns an integer with only the first numBits bits of this value, sign - * extended from the final bit. - * @param {number} numBits The number of bits by which to shift. - * @return {!Integer} The shorted integer value. - */ -Integer.prototype.shorten = function (numBits) { - let arrIndex = (numBits - 1) >> 5; - let bitIndex = (numBits - 1) % 32; - let bits = []; - for (let i = 0; i < arrIndex; i++) { - bits[i] = this.getBits(i); - } - let sigBits = bitIndex == 31 ? 0xffffffff : (1 << (bitIndex + 1)) - 1; - let val = this.getBits(arrIndex) & sigBits; - // noinspection JSBitwiseOperatorUsage - if (val & (1 << bitIndex)) { - val |= 0xffffffff - sigBits; - bits[arrIndex] = val; - return new Integer(bits, -1); + /** + * @param {Integer} other Integer to compare against. + * @return {boolean} Whether this Integer is less than the other. + */ + lessThan(other) { + return this.compare(other) < 0; } - bits[arrIndex] = val; - return new Integer(bits, 0); -}; - -/** @return {!Integer} The negation of this value. */ -Integer.prototype.negate = function () { - return this.not().add(Integer.ONE); -}; - -/** - * Returns the sum of this and the given Integer. - * @param {Integer} other The Integer to add to this. - * @return {!Integer} The Integer result. - */ -Integer.prototype.add = function (other) { - let len = Math.max(this.bits_.length, other.bits_.length); - let arr = []; - let carry = 0; - - for (let i = 0; i <= len; i++) { - let a1 = this.getBits(i) >>> 16; - let a0 = this.getBits(i) & 0xffff; - - let b1 = other.getBits(i) >>> 16; - let b0 = other.getBits(i) & 0xffff; - - let c0 = carry + a0 + b0; - let c1 = (c0 >>> 16) + a1 + b1; - carry = c1 >>> 16; - c0 &= 0xffff; - c1 &= 0xffff; - arr[i] = (c1 << 16) | c0; + /** + * @param {Integer} other Integer to compare against. + * @return {boolean} Whether this Integer is less than or equal to the other. + */ + lessThanOrEqual(other) { + return this.compare(other) <= 0; } - return Integer.fromBits(arr); -}; - -/** - * Returns the difference of this and the given Integer. - * @param {Integer} other The Integer to subtract from this. - * @return {!Integer} The Integer result. - */ -Integer.prototype.subtract = function (other) { - return this.add(other.negate()); -}; - -/** - * Returns the product of this and the given Integer. - * @param {Integer} other The Integer to multiply against this. - * @return {!Integer} The product of this and the other. - */ -Integer.prototype.multiply = function (other) { - if (this.isZero()) { - return Integer.ZERO; - } else if (other.isZero()) { - return Integer.ZERO; + /** + * Compares this Integer with the given one. + * @param {Integer} other Integer to compare against. + * @return {number} 0 if they are the same, 1 if the this is greater, and -1 + * if the given one is greater. + */ + compare(other) { + let diff = this.subtract(other); + if (diff.isNegative()) { + return -1; + } else if (diff.isZero()) { + return 0; + } + return +1; } - - if (this.isNegative()) { - if (other.isNegative()) { - return this.negate().multiply(other.negate()); + /** + * Returns an integer with only the first numBits bits of this value, sign + * extended from the final bit. + * @param {number} numBits The number of bits by which to shift. + * @return {!Integer} The shorted integer value. + */ + shorten(numBits) { + let arrIndex = (numBits - 1) >> 5; + let bitIndex = (numBits - 1) % 32; + let bits = []; + for (let i = 0; i < arrIndex; i++) { + bits[i] = this.getBits(i); + } + let sigBits = bitIndex == 31 ? 0xffffffff : (1 << (bitIndex + 1)) - 1; + let val = this.getBits(arrIndex) & sigBits; + // noinspection JSBitwiseOperatorUsage + if (val & (1 << bitIndex)) { + val |= 0xffffffff - sigBits; + bits[arrIndex] = val; + return new Integer(bits, -1); } - return this.negate().multiply(other).negate(); - } else if (other.isNegative()) { - return this.multiply(other.negate()).negate(); + bits[arrIndex] = val; + return new Integer(bits, 0); } - - // If both numbers are small, use float multiplication - if ( - this.lessThan(Integer.TWO_PWR_24_) && - other.lessThan(Integer.TWO_PWR_24_) - ) { - return Integer.fromNumber(this.toNumber() * other.toNumber()); + /** @return {!Integer} The negation of this value. */ + negate() { + return this.not().add(Integer.ONE); } + /** + * Returns the sum of this and the given Integer. + * @param {Integer} other The Integer to add to this. + * @return {!Integer} The Integer result. + */ + add(other) { + let len = Math.max(this.bits_.length, other.bits_.length); + let arr = []; + let carry = 0; - // Fill in an array of 16-bit products. - let len = this.bits_.length + other.bits_.length; - let arr = []; - for (let i = 0; i < 2 * len; i++) { - arr[i] = 0; - } - for (let i = 0; i < this.bits_.length; i++) { - for (let j = 0; j < other.bits_.length; j++) { + for (let i = 0; i <= len; i++) { let a1 = this.getBits(i) >>> 16; let a0 = this.getBits(i) & 0xffff; - let b1 = other.getBits(j) >>> 16; - let b0 = other.getBits(j) & 0xffff; + let b1 = other.getBits(i) >>> 16; + let b0 = other.getBits(i) & 0xffff; - arr[2 * i + 2 * j] += a0 * b0; - Integer.carry16_(arr, 2 * i + 2 * j); - arr[2 * i + 2 * j + 1] += a1 * b0; - Integer.carry16_(arr, 2 * i + 2 * j + 1); - arr[2 * i + 2 * j + 1] += a0 * b1; - Integer.carry16_(arr, 2 * i + 2 * j + 1); - arr[2 * i + 2 * j + 2] += a1 * b1; - Integer.carry16_(arr, 2 * i + 2 * j + 2); + let c0 = carry + a0 + b0; + let c1 = (c0 >>> 16) + a1 + b1; + carry = c1 >>> 16; + c0 &= 0xffff; + c1 &= 0xffff; + arr[i] = (c1 << 16) | c0; } + return Integer.fromBits(arr); } - - // Combine the 16-bit values into 32-bit values. - for (let i = 0; i < len; i++) { - arr[i] = (arr[2 * i + 1] << 16) | arr[2 * i]; - } - for (let i = len; i < 2 * len; i++) { - arr[i] = 0; - } - return new Integer(arr, 0); -}; - -/** - * Carries any overflow from the given index into later entries. - * @param {Array.} bits Array of 16-bit values in little-endian order. - * @param {number} index The index in question. - * @private - */ -Integer.carry16_ = function (bits, index) { - while ((bits[index] & 0xffff) != bits[index]) { - bits[index + 1] += bits[index] >>> 16; - bits[index] &= 0xffff; - } -}; - -/** - * Returns this Integer divided by the given one. - * @param {Integer} other Th Integer to divide this by. - * @return {!Integer} This value divided by the given one. - */ -Integer.prototype.divide = function (other) { - if (other.isZero()) { - throw Error("division by zero"); - } else if (this.isZero()) { - return Integer.ZERO; + /** + * Returns the difference of this and the given Integer. + * @param {Integer} other The Integer to subtract from this. + * @return {!Integer} The Integer result. + */ + subtract(other) { + return this.add(other.negate()); } + /** + * Returns the product of this and the given Integer. + * @param {Integer} other The Integer to multiply against this. + * @return {!Integer} The product of this and the other. + */ + multiply(other) { + if (this.isZero()) { + return Integer.ZERO; + } else if (other.isZero()) { + return Integer.ZERO; + } - if (this.isNegative()) { - if (other.isNegative()) { - return this.negate().divide(other.negate()); + if (this.isNegative()) { + if (other.isNegative()) { + return this.negate().multiply(other.negate()); + } + return this.negate().multiply(other).negate(); + } else if (other.isNegative()) { + return this.multiply(other.negate()).negate(); } - return this.negate().divide(other).negate(); - } else if (other.isNegative()) { - return this.divide(other.negate()).negate(); - } - // Repeat the following until the remainder is less than other: find a - // floating-point that approximates remainder / other *from below*, add this - // into the result, and subtract it from the remainder. It is critical that - // the approximate value is less than or equal to the real value so that the - // remainder never becomes negative. - let res = Integer.ZERO; - let rem = this; - while (rem.greaterThanOrEqual(other)) { - // Approximate the result of division. This may be a little greater or - // smaller than the actual value. - let approx = Math.max(1, Math.floor(rem.toNumber() / other.toNumber())); + // If both numbers are small, use float multiplication + if ( + this.lessThan(Integer.TWO_PWR_24_) && + other.lessThan(Integer.TWO_PWR_24_) + ) { + return Integer.fromNumber(this.toNumber() * other.toNumber()); + } - // We will tweak the approximate result by changing it in the 48-th digit or - // the smallest non-fractional digit, whichever is larger. - let log2 = Math.ceil(Math.log(approx) / Math.LN2); - let delta = log2 <= 48 ? 1 : Math.pow(2, log2 - 48); + // Fill in an array of 16-bit products. + let len = this.bits_.length + other.bits_.length; + let arr = []; + for (let i = 0; i < 2 * len; i++) { + arr[i] = 0; + } + for (let i = 0; i < this.bits_.length; i++) { + for (let j = 0; j < other.bits_.length; j++) { + let a1 = this.getBits(i) >>> 16; + let a0 = this.getBits(i) & 0xffff; + + let b1 = other.getBits(j) >>> 16; + let b0 = other.getBits(j) & 0xffff; + + arr[2 * i + 2 * j] += a0 * b0; + Integer.carry16_(arr, 2 * i + 2 * j); + arr[2 * i + 2 * j + 1] += a1 * b0; + Integer.carry16_(arr, 2 * i + 2 * j + 1); + arr[2 * i + 2 * j + 1] += a0 * b1; + Integer.carry16_(arr, 2 * i + 2 * j + 1); + arr[2 * i + 2 * j + 2] += a1 * b1; + Integer.carry16_(arr, 2 * i + 2 * j + 2); + } + } - // Decrease the approximation until it is smaller than the remainder. Note - // that if it is too large, the product overflows and is negative. - let approxRes = Integer.fromNumber(approx); - let approxRem = approxRes.multiply(other); - while (approxRem.isNegative() || approxRem.greaterThan(rem)) { - approx -= delta; - approxRes = Integer.fromNumber(approx); - approxRem = approxRes.multiply(other); + // Combine the 16-bit values into 32-bit values. + for (let i = 0; i < len; i++) { + arr[i] = (arr[2 * i + 1] << 16) | arr[2 * i]; + } + for (let i = len; i < 2 * len; i++) { + arr[i] = 0; + } + return new Integer(arr, 0); + } + /** + * Returns this Integer divided by the given one. + * @param {Integer} other Th Integer to divide this by. + * @return {!Integer} This value divided by the given one. + */ + divide(other) { + if (other.isZero()) { + throw Error("division by zero"); + } else if (this.isZero()) { + return Integer.ZERO; } - // We know the answer can't be zero... and actually, zero would cause - // infinite recursion since we would make no progress. - if (approxRes.isZero()) { - approxRes = Integer.ONE; + if (this.isNegative()) { + if (other.isNegative()) { + return this.negate().divide(other.negate()); + } + return this.negate().divide(other).negate(); + } else if (other.isNegative()) { + return this.divide(other.negate()).negate(); } - res = res.add(approxRes); - rem = rem.subtract(approxRem); - } - return res; -}; + // Repeat the following until the remainder is less than other: find a + // floating-point that approximates remainder / other *from below*, add this + // into the result, and subtract it from the remainder. It is critical that + // the approximate value is less than or equal to the real value so that the + // remainder never becomes negative. + let res = Integer.ZERO; + let rem = this; + while (rem.greaterThanOrEqual(other)) { + // Approximate the result of division. This may be a little greater or + // smaller than the actual value. + let approx = Math.max( + 1, + Math.floor(rem.toNumber() / other.toNumber()), + ); + + // We will tweak the approximate result by changing it in the 48-th digit or + // the smallest non-fractional digit, whichever is larger. + let log2 = Math.ceil(Math.log(approx) / Math.LN2); + let delta = log2 <= 48 ? 1 : Math.pow(2, log2 - 48); + + // Decrease the approximation until it is smaller than the remainder. Note + // that if it is too large, the product overflows and is negative. + let approxRes = Integer.fromNumber(approx); + let approxRem = approxRes.multiply(other); + while (approxRem.isNegative() || approxRem.greaterThan(rem)) { + approx -= delta; + approxRes = Integer.fromNumber(approx); + approxRem = approxRes.multiply(other); + } -/** - * Returns this Integer modulo the given one. - * @param {Integer} other The Integer by which to mod. - * @return {!Integer} This value modulo the given one. - */ -Integer.prototype.modulo = function (other) { - return this.subtract(this.divide(other).multiply(other)); -}; + // We know the answer can't be zero... and actually, zero would cause + // infinite recursion since we would make no progress. + if (approxRes.isZero()) { + approxRes = Integer.ONE; + } -/** @return {!Integer} The bitwise-NOT of this value. */ -Integer.prototype.not = function () { - let len = this.bits_.length; - let arr = []; - for (let i = 0; i < len; i++) { - arr[i] = ~this.bits_[i]; + res = res.add(approxRes); + rem = rem.subtract(approxRem); + } + return res; } - return new Integer(arr, ~this.sign_); -}; - -/** - * Returns the bitwise-AND of this Integer and the given one. - * @param {Integer} other The Integer to AND with this. - * @return {!Integer} The bitwise-AND of this and the other. - */ -Integer.prototype.and = function (other) { - let len = Math.max(this.bits_.length, other.bits_.length); - let arr = []; - for (let i = 0; i < len; i++) { - arr[i] = this.getBits(i) & other.getBits(i); + /** + * Returns this Integer modulo the given one. + * @param {Integer} other The Integer by which to mod. + * @return {!Integer} This value modulo the given one. + */ + modulo(other) { + return this.subtract(this.divide(other).multiply(other)); + } + /** @return {!Integer} The bitwise-NOT of this value. */ + not() { + let len = this.bits_.length; + let arr = []; + for (let i = 0; i < len; i++) { + arr[i] = ~this.bits_[i]; + } + return new Integer(arr, ~this.sign_); } - return new Integer(arr, this.sign_ & other.sign_); -}; - -/** - * Returns the bitwise-OR of this Integer and the given one. - * @param {Integer} other The Integer to OR with this. - * @return {!Integer} The bitwise-OR of this and the other. - */ -Integer.prototype.or = function (other) { - let len = Math.max(this.bits_.length, other.bits_.length); - let arr = []; - for (let i = 0; i < len; i++) { - arr[i] = this.getBits(i) | other.getBits(i); + /** + * Returns the bitwise-AND of this Integer and the given one. + * @param {Integer} other The Integer to AND with this. + * @return {!Integer} The bitwise-AND of this and the other. + */ + and(other) { + let len = Math.max(this.bits_.length, other.bits_.length); + let arr = []; + for (let i = 0; i < len; i++) { + arr[i] = this.getBits(i) & other.getBits(i); + } + return new Integer(arr, this.sign_ & other.sign_); } - return new Integer(arr, this.sign_ | other.sign_); -}; - -/** - * Returns the bitwise-XOR of this Integer and the given one. - * @param {Integer} other The Integer to XOR with this. - * @return {!Integer} The bitwise-XOR of this and the other. - */ -Integer.prototype.xor = function (other) { - let len = Math.max(this.bits_.length, other.bits_.length); - let arr = []; - for (let i = 0; i < len; i++) { - arr[i] = this.getBits(i) ^ other.getBits(i); + /** + * Returns the bitwise-OR of this Integer and the given one. + * @param {Integer} other The Integer to OR with this. + * @return {!Integer} The bitwise-OR of this and the other. + */ + or(other) { + let len = Math.max(this.bits_.length, other.bits_.length); + let arr = []; + for (let i = 0; i < len; i++) { + arr[i] = this.getBits(i) | other.getBits(i); + } + return new Integer(arr, this.sign_ | other.sign_); } - return new Integer(arr, this.sign_ ^ other.sign_); -}; + /** + * Returns the bitwise-XOR of this Integer and the given one. + * @param {Integer} other The Integer to XOR with this. + * @return {!Integer} The bitwise-XOR of this and the other. + */ + xor(other) { + let len = Math.max(this.bits_.length, other.bits_.length); + let arr = []; + for (let i = 0; i < len; i++) { + arr[i] = this.getBits(i) ^ other.getBits(i); + } + return new Integer(arr, this.sign_ ^ other.sign_); + } + /** + * Returns this value with bits shifted to the left by the given amount. + * @param {number} numBits The number of bits by which to shift. + * @return {!Integer} This shifted to the left by the given amount. + */ + shiftLeft(numBits) { + let arrDelta = numBits >> 5; + let bitDelta = numBits % 32; + let len = this.bits_.length + arrDelta + (bitDelta > 0 ? 1 : 0); + let arr = []; + for (let i = 0; i < len; i++) { + if (bitDelta > 0) { + arr[i] = + (this.getBits(i - arrDelta) << bitDelta) | + (this.getBits(i - arrDelta - 1) >>> (32 - bitDelta)); + } else { + arr[i] = this.getBits(i - arrDelta); + } + } + return new Integer(arr, this.sign_); + } + /** + * Returns this value with bits shifted to the right by the given amount. + * @param {number} numBits The number of bits by which to shift. + * @return {!Integer} This shifted to the right by the given amount. + */ + shiftRight(numBits) { + let arrDelta = numBits >> 5; + let bitDelta = numBits % 32; + let len = this.bits_.length - arrDelta; + let arr = []; + for (let i = 0; i < len; i++) { + if (bitDelta > 0) { + arr[i] = + (this.getBits(i + arrDelta) >>> bitDelta) | + (this.getBits(i + arrDelta + 1) << (32 - bitDelta)); + } else { + arr[i] = this.getBits(i + arrDelta); + } + } + return new Integer(arr, this.sign_); + } + /** + * Provide the name of the constructor and the string representation + * @returns {string} + */ + inspect() { + return this.constructor.name + ": " + this.toString(); + } + /** + * Returns a Integer whose value is the absolute value of this + * @returns {Integer} + */ + abs() { + return this.sign_ === 0 ? this : this.negate(); + } + /** + * Returns the string representation. + * Method used by the native JSON.stringify() to serialize this instance. + */ + toJSON() { + return this.toString(); + } +} -/** - * Returns this value with bits shifted to the left by the given amount. - * @param {number} numBits The number of bits by which to shift. - * @return {!Integer} This shifted to the left by the given amount. - */ -Integer.prototype.shiftLeft = function (numBits) { - let arrDelta = numBits >> 5; - let bitDelta = numBits % 32; - let len = this.bits_.length + arrDelta + (bitDelta > 0 ? 1 : 0); - let arr = []; - for (let i = 0; i < len; i++) { - if (bitDelta > 0) { - arr[i] = - (this.getBits(i - arrDelta) << bitDelta) | - (this.getBits(i - arrDelta - 1) >>> (32 - bitDelta)); - } else { - arr[i] = this.getBits(i - arrDelta); - } - } - return new Integer(arr, this.sign_); -}; +// NOTE: Common constant values ZERO, ONE, NEG_ONE, etc. are defined below the +// from* methods on which they depend. /** - * Returns this value with bits shifted to the right by the given amount. - * @param {number} numBits The number of bits by which to shift. - * @return {!Integer} This shifted to the right by the given amount. + * A cache of the Integer representations of small integer values. + * @type {!Object} + * @private */ -Integer.prototype.shiftRight = function (numBits) { - let arrDelta = numBits >> 5; - let bitDelta = numBits % 32; - let len = this.bits_.length - arrDelta; - let arr = []; - for (let i = 0; i < len; i++) { - if (bitDelta > 0) { - arr[i] = - (this.getBits(i + arrDelta) >>> bitDelta) | - (this.getBits(i + arrDelta + 1) << (32 - bitDelta)); - } else { - arr[i] = this.getBits(i + arrDelta); - } - } - return new Integer(arr, this.sign_); -}; +Integer.IntCache_ = {}; /** - * Provide the name of the constructor and the string representation - * @returns {string} + * A number used repeatedly in calculations. This must appear before the first + * call to the from* functions below. + * @type {number} + * @private */ -Integer.prototype.inspect = function () { - return this.constructor.name + ": " + this.toString(); -}; +Integer.TWO_PWR_32_DBL_ = (1 << 16) * (1 << 16); -/** - * Returns a Integer whose value is the absolute value of this - * @returns {Integer} - */ -Integer.prototype.abs = function () { - return this.sign_ === 0 ? this : this.negate(); -}; +/** @type {!Integer} */ +Integer.ZERO = Integer.fromInt(0); + +/** @type {!Integer} */ +Integer.ONE = Integer.fromInt(1); /** - * Returns the string representation. - * Method used by the native JSON.stringify() to serialize this instance. + * @type {!Integer} + * @private */ -Integer.prototype.toJSON = function () { - return this.toString(); -}; +Integer.TWO_PWR_24_ = Integer.fromInt(1 << 24); module.exports = Integer; From 7957477cdf1b978c5526626e38bb89e9453dc314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Mon, 9 Dec 2024 14:39:05 +0100 Subject: [PATCH 07/52] Mark integer as deprecated --- lib/client-options.js | 6 ++++-- lib/encoder.js | 3 +++ lib/types/integer.js | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/client-options.js b/lib/client-options.js index 5788650d..b9521bf2 100644 --- a/lib/client-options.js +++ b/lib/client-options.js @@ -249,8 +249,10 @@ const { throwNotSupported } = require("./new-utils"); * @property {Boolean} [encoding.useBigIntAsLong] Use [BigInt ECMAScript type](https://tc39.github.io/proposal-bigint/) * to represent CQL bigint and counter data types. * [TODO: Add support for this field] - * @property {Boolean} [encoding.useBigIntAsVarint] Use [BigInt ECMAScript - * type](https://tc39.github.io/proposal-bigint/) to represent CQL varint data type. + * @property {Boolean} [encoding.useBigIntAsVarint] Use [BigInt ECMAScript type](https://tc39.github.io/proposal-bigint/) + * to represent CQL varint data type. + * + * Note, that using Integer as Varint (``useBigIntAsVarint == false``) is deprecated. * [TODO: Add support for this field] * @property {Array.} [profiles] The array of [execution profiles]{@link ExecutionProfile}. * [TODO: Add support for this field] diff --git a/lib/encoder.js b/lib/encoder.js index 9b6e3d5f..be77c188 100644 --- a/lib/encoder.js +++ b/lib/encoder.js @@ -4,6 +4,9 @@ const util = require("util"); const types = require("./types"); const dataTypes = types.dataTypes; const Long = types.Long; +/** + * @deprecated Integer is deprecated. See ``./types/integer.js`` + */ const Integer = types.Integer; const BigDecimal = types.BigDecimal; const MutableLong = require("./types/mutable-long"); diff --git a/lib/types/integer.js b/lib/types/integer.js index cb8f2a6a..2bf83ca0 100644 --- a/lib/types/integer.js +++ b/lib/types/integer.js @@ -12,6 +12,8 @@ const utils = require("../utils"); * representation without overflow or change in sign. * * @final + * @deprecated Use either Long or builtin BigInt type instead of Integer. + * This class will be remover at a later time. */ class Integer { /** From 1a08fbca49f56cfa64868e6b0574b5a5a1555aa2 Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Sun, 24 Nov 2024 23:01:07 +0100 Subject: [PATCH 08/52] Refactor LocalDate to class in JS --- lib/types/local-date.js | 431 ++++++++++++++++++++-------------------- 1 file changed, 221 insertions(+), 210 deletions(-) diff --git a/lib/types/local-date.js b/lib/types/local-date.js index 30bf2d85..70b2e621 100644 --- a/lib/types/local-date.js +++ b/lib/types/local-date.js @@ -1,256 +1,269 @@ "use strict"; -const util = require("util"); - const utils = require("../utils"); /** @module types */ /** - * @private + * Maximum number of days from 01.01.1970 supported by the class (i32::MAX in Rust).. * @const */ -const millisecondsPerDay = 86400000; +const maxDay = 2147483647; + /** - * @private + * Minimum number of days from 01.01.1970 supported by the class (i32::MIN in Rust). + * @const */ +const minDay = -2147483648; + +/** + * @const + */ +const millisecondsPerDay = 86400000; + const dateCenter = Math.pow(2, 31); /** + * A date without a time-zone in the ISO-8601 calendar system, such as 2010-08-05. * - * Creates a new instance of LocalDate. - * @class - * @classdesc A date without a time-zone in the ISO-8601 calendar system, such as 2010-08-05. - *

- * LocalDate is an immutable object that represents a date, often viewed as year-month-day. For example, the value "1st October 2014" can be stored in a LocalDate. - *

- *

- * This class does not store or represent a time or time-zone. Instead, it is a description of the date, as used for birthdays. It cannot represent an instant on the time-line without additional information such as an offset or time-zone. - *

- *

- * Note that this type can represent dates in the range [-5877641-06-23; 5881580-07-17] while the ES5 date type can only represent values in the range of [-271821-04-20; 275760-09-13]. - * In the event that year, month, day parameters do not fall within the ES5 date range an Error will be thrown. If you wish to represent a date outside of this range, pass a single - * parameter indicating the days since epoch. For example, -1 represents 1969-12-31. - *

- * @param {Number} year The year or days since epoch. If days since epoch, month and day should not be provided. - * @param {Number} month Between 1 and 12 inclusive. - * @param {Number} day Between 1 and the number of days in the given month of the given year. + * LocalDate is an immutable object that represents a date, often viewed as year-month-day. For example, the value "1st October 2014" can be stored in a LocalDate. * - * @property {Date} date The date representation if falls within a range of an ES5 data type, otherwise an invalid date. + * This class does not store or represent a time or time-zone. Instead, it is a description of the date, as used for birthdays. + * It cannot represent an instant on the time-line without additional information such as an offset or time-zone. * - * @constructor + * Note that this type can represent dates in the range [-5877641-06-23; 5881580-07-17] + * while the ES5 date type can only represent values in the range of [-271821-04-20; 275760-09-13]. + * In the event that year, month, day parameters do not fall within the ES5 date range an Error will be thrown. + * If you wish to represent a date outside of this range, pass a single + * parameter indicating the days since epoch. For example, -1 represents 1969-12-31. + * @property {Date} date The date representation if falls within a range of an ES5 data type, otherwise an invalid date. */ -function LocalDate(year, month, day) { - // implementation detail: internally uses a UTC based date - if ( - typeof year === "number" && - typeof month === "number" && - typeof day === "number" - ) { - // Use setUTCFullYear as if there is a 2 digit year, Date.UTC() assumes - // that is the 20th century. - this.date = new Date(); - this.date.setUTCHours(0, 0, 0, 0); - this.date.setUTCFullYear(year, month - 1, day); - if (isNaN(this.date.getTime())) { - throw new Error( - util.format( - "%d-%d-%d does not form a valid ES5 date!", - year, - month, - day, - ), - ); - } - } else if (typeof month === "undefined" && typeof day === "undefined") { - if (typeof year === "number") { - // in days since epoch. - if (year < -2147483648 || year > 2147483647) { +class LocalDate { + /** + * Creates a new instance of LocalDate. + * @param {Number} year The year or days since epoch. If days since epoch, month and day should not be provided. + * @param {Number} month Between 1 and 12 inclusive. + * @param {Number} day Between 1 and the number of days in the given month of the given year. + */ + constructor(year, month, day) { + // implementation detail: internally uses a UTC based date + if ( + typeof year === "number" && + typeof month === "number" && + typeof day === "number" + ) { + // Use setUTCFullYear as if there is a 2 digit year, Date.UTC() assumes + // that is the 20th century. + this.date = new Date(); + this.date.setUTCHours(0, 0, 0, 0); + this.date.setUTCFullYear(year, month - 1, day); + if (isNaN(this.date.getTime())) { throw new Error( - "You must provide a valid value for days since epoch (-2147483648 <= value <= 2147483647).", + util.format( + "%d-%d-%d does not form a valid ES5 date!", + year, + month, + day, + ), ); } - this.date = new Date(year * millisecondsPerDay); + } else if (typeof month === "undefined" && typeof day === "undefined") { + if (typeof year === "number") { + // In days since epoch. + if (year < minDay || year > maxDay) { + throw new Error( + `You must provide a valid value for days since epoch (${minDay} <= value <= ${maxDay}).`, + ); + } + this.date = new Date(year * millisecondsPerDay); + } + } + + if (typeof this.date === "undefined") { + throw new Error("You must provide a valid year, month and day"); } - } - if (typeof this.date === "undefined") { - throw new Error("You must provide a valid year, month and day"); + /** + * If date cannot be represented yet given a valid days since epoch, track + * it internally. + */ + this._value = isNaN(this.date.getTime()) ? year : null; + + /** + * A number representing the year. May return NaN if cannot be represented as + * a Date. + * @type Number + */ + this.year = this.date.getUTCFullYear(); + /** + * A number between 1 and 12 inclusive representing the month. May return + * NaN if cannot be represented as a Date. + * @type Number + */ + this.month = this.date.getUTCMonth() + 1; + /** + * A number between 1 and the number of days in the given month of the given year (value up to 31). + * May return NaN if cannot be represented as a Date. + * @type Number + */ + this.day = this.date.getUTCDate(); } /** - * If date cannot be represented yet given a valid days since epoch, track - * it internally. + * Creates a new instance of LocalDate using the current year, month and day from the system clock in the default time-zone. */ - this._value = isNaN(this.date.getTime()) ? year : null; + static now() { + return LocalDate.fromDate(new Date()); + } /** - * A number representing the year. May return NaN if cannot be represented as - * a Date. - * @type Number + * Creates a new instance of LocalDate using the current date from the system clock at UTC. */ - this.year = this.date.getUTCFullYear(); + static utcNow() { + return LocalDate.fromDate(Date.now()); + } + /** - * A number between 1 and 12 inclusive representing the month. May return - * NaN if cannot be represented as a Date. - * @type Number + * Creates a new instance of LocalDate using the year, month and day from the provided local date time. + * @param {Date} date + * @returns {LocalDate} */ - this.month = this.date.getUTCMonth() + 1; + static fromDate(date) { + if (isNaN(date.getTime())) { + throw new TypeError(`Invalid date: ${date}`); + } + return new LocalDate( + date.getFullYear(), + date.getMonth() + 1, // getMonth() returns the month index 0..11, and the need for a number 1..12 + date.getDate(), + ); + } + /** - * A number between 1 and the number of days in the given month of the given year (28, 29, 30, 31). - * May return NaN if cannot be represented as a Date. - * @type Number + * Creates a new instance of LocalDate using the year, month and day provided in the form: yyyy-mm-dd or + * days since epoch (i.e. -1 for Dec 31, 1969). + * @param {string} value + * @returns {LocalDate} */ - this.day = this.date.getUTCDate(); -} - -/** - * Creates a new instance of LocalDate using the current year, month and day from the system clock in the default time-zone. - */ -LocalDate.now = function () { - return LocalDate.fromDate(new Date()); -}; - -/** - * Creates a new instance of LocalDate using the current date from the system clock at UTC. - */ -LocalDate.utcNow = function () { - return new LocalDate(Date.now()); -}; + static fromString(value) { + const dashCount = (value.match(/-/g) || []).length; + if (dashCount >= 2) { + let multiplier = 1; + if (value[0] === "-") { + value = value.substring(1); + multiplier = -1; + } + const parts = value.split("-"); + return new LocalDate( + multiplier * parseInt(parts[0], 10), + parseInt(parts[1], 10), + parseInt(parts[2], 10), + ); + } + if (value.match(/^-?\d+$/)) { + // Parse as days since epoch. + return new LocalDate(parseInt(value, 10)); + } + throw new Error(`Invalid input ${value}.`); + } -/** - * Creates a new instance of LocalDate using the year, month and day from the provided local date time. - * @param {Date} date - */ -LocalDate.fromDate = function (date) { - if (isNaN(date.getTime())) { - throw new TypeError("Invalid date: " + date); + /** + * Creates a new instance of LocalDate using the bytes representation. + * @param {Buffer} buffer + * @returns {LocalDate} + */ + static fromBuffer(buffer) { + // move to unix epoch: 0. + return new LocalDate(buffer.readUInt32BE(0) - dateCenter); } - return new LocalDate( - date.getFullYear(), - date.getMonth() + 1, - date.getDate(), - ); -}; -/** - * Creates a new instance of LocalDate using the year, month and day provided in the form: yyyy-mm-dd or - * days since epoch (i.e. -1 for Dec 31, 1969). - * @param {String} value - */ -LocalDate.fromString = function (value) { - const dashCount = (value.match(/-/g) || []).length; - if (dashCount >= 2) { - let multiplier = 1; - if (value[0] === "-") { - value = value.substring(1); - multiplier = -1; + /** + * Compares this LocalDate with the given one. + * @param {LocalDate} other date to compare against. + * @return {number} 0 if they are the same, 1 if the this is greater, and -1 + * if the given one is greater. + */ + compare(other) { + const thisValue = isNaN(this.date.getTime()) + ? this._value * millisecondsPerDay + : this.date.getTime(); + const otherValue = isNaN(other.date.getTime()) + ? other._value * millisecondsPerDay + : other.date.getTime(); + const diff = thisValue - otherValue; + if (diff < 0) { + return -1; } - const parts = value.split("-"); - return new LocalDate( - multiplier * parseInt(parts[0], 10), - parseInt(parts[1], 10), - parseInt(parts[2], 10), - ); - } - if (value.match(/^-?\d+$/)) { - // Parse as days since epoch. - return new LocalDate(parseInt(value, 10)); + if (diff > 0) { + return 1; + } + return 0; } - throw new Error("Invalid input '" + value + "'."); -}; -/** - * Creates a new instance of LocalDate using the bytes representation. - * @param {Buffer} buffer - */ -LocalDate.fromBuffer = function (buffer) { - // move to unix epoch: 0. - return new LocalDate(buffer.readUInt32BE(0) - dateCenter); -}; - -/** - * Compares this LocalDate with the given one. - * @param {LocalDate} other date to compare against. - * @return {number} 0 if they are the same, 1 if the this is greater, and -1 - * if the given one is greater. - */ -LocalDate.prototype.compare = function (other) { - const thisValue = isNaN(this.date.getTime()) - ? this._value * millisecondsPerDay - : this.date.getTime(); - const otherValue = isNaN(other.date.getTime()) - ? other._value * millisecondsPerDay - : other.date.getTime(); - const diff = thisValue - otherValue; - if (diff < 0) { - return -1; + /** + * Returns true if the value of the LocalDate instance and other are the same + * @param {LocalDate} other + * @returns {Boolean} + */ + equals(other) { + return other instanceof LocalDate && this.compare(other) === 0; } - if (diff > 0) { - return 1; + /** + * Provide the name of the constructor and the string representation + * @returns {string} + */ + inspect() { + return `${this.constructor.name} : ${this.toString()}`; } - return 0; -}; - -/** - * Returns true if the value of the LocalDate instance and other are the same - * @param {LocalDate} other - * @returns {Boolean} - */ -LocalDate.prototype.equals = function (other) { - return other instanceof LocalDate && this.compare(other) === 0; -}; - -LocalDate.prototype.inspect = function () { - return this.constructor.name + ": " + this.toString(); -}; -/** - * Gets the bytes representation of the instance. - * @returns {Buffer} - */ -LocalDate.prototype.toBuffer = function () { - // days since unix epoch - const daysSinceEpoch = isNaN(this.date.getTime()) - ? this._value - : Math.floor(this.date.getTime() / millisecondsPerDay); - const value = daysSinceEpoch + dateCenter; - const buf = utils.allocBufferUnsafe(4); - buf.writeUInt32BE(value, 0); - return buf; -}; - -/** - * Gets the string representation of the instance in the form: yyyy-mm-dd if - * the value can be parsed as a Date, otherwise days since epoch. - * @returns {String} - */ -LocalDate.prototype.toString = function () { - let result; - // if cannot be parsed as date, return days since epoch representation. - if (isNaN(this.date.getTime())) { - return this._value.toString(); + /** + * Gets the bytes representation of the instance. + * @returns {Buffer} + */ + toBuffer() { + // days since unix epoch + const daysSinceEpoch = isNaN(this.date.getTime()) + ? this._value + : Math.floor(this.date.getTime() / millisecondsPerDay); + const value = daysSinceEpoch + dateCenter; + const buf = utils.allocBufferUnsafe(4); + buf.writeUInt32BE(value, 0); + return buf; } - if (this.year < 0) { - result = "-" + fillZeros((this.year * -1).toString(), 4); - } else { - result = fillZeros(this.year.toString(), 4); + + /** + * Gets the string representation of the instance in the form: yyyy-mm-dd if + * the value can be parsed as a Date, otherwise days since epoch. + * @returns {string} + */ + toString() { + let result; + // if cannot be parsed as date, return days since epoch representation. + if (isNaN(this.date.getTime())) { + return this._value.toString(); + } + if (this.year < 0) { + result = "-" + fillZeros((this.year * -1).toString(), 4); + } else { + result = fillZeros(this.year.toString(), 4); + } + result += + "-" + + fillZeros(this.month.toString(), 2) + + "-" + + fillZeros(this.day.toString(), 2); + return result; } - result += - "-" + - fillZeros(this.month.toString(), 2) + - "-" + - fillZeros(this.day.toString(), 2); - return result; -}; -/** - * Gets the string representation of the instance in the form: yyyy-mm-dd, valid for JSON. - * @returns {String} - */ -LocalDate.prototype.toJSON = function () { - return this.toString(); -}; + /** + * Gets the string representation of the instance in the form: yyyy-mm-dd, valid for JSON. + * @returns {string} + */ + toJSON() { + return this.toString(); + } +} +module.exports = LocalDate; /** - * @param {String} value + * @param {string} value * @param {Number} amount * @private */ @@ -260,5 +273,3 @@ function fillZeros(value, amount) { } return utils.stringRepeat("0", amount - value.length) + value; } - -module.exports = LocalDate; From 69d39ffd781fa0bfbfa8187bc06cf7860dbd2049 Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Sun, 24 Nov 2024 23:16:55 +0100 Subject: [PATCH 09/52] Add tests for LocalDate Move tests for LocalDate to new file. Add tests for errors in setters. --- test/unit-not-supported/basic-tests.js | 87 ------------ test/unit/local-date-tests.js | 178 +++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 87 deletions(-) create mode 100644 test/unit/local-date-tests.js diff --git a/test/unit-not-supported/basic-tests.js b/test/unit-not-supported/basic-tests.js index 3e0ea44d..44de37ce 100644 --- a/test/unit-not-supported/basic-tests.js +++ b/test/unit-not-supported/basic-tests.js @@ -196,93 +196,6 @@ describe("types", function () { }); }); }); - describe("LocalDate", function () { - const LocalDate = types.LocalDate; - describe("new LocalDate", function () { - it("should refuse to create LocalDate from invalid values.", function () { - assert.throws(() => new types.LocalDate(), Error); - assert.throws(() => new types.LocalDate(undefined), Error); - // Outside of ES5 Date range. - assert.throws(() => new types.LocalDate(-271821, 4, 19), Error); - assert.throws(() => new types.LocalDate(275760, 9, 14), Error); - // Outside of LocalDate range. - assert.throws(() => new types.LocalDate(-2147483649), Error); - assert.throws(() => new types.LocalDate(2147483648), Error); - }); - }); - describe("#toString()", function () { - it("should return the string in the form of yyyy-mm-dd", function () { - assert.strictEqual( - new LocalDate(2015, 2, 1).toString(), - "2015-02-01", - ); - assert.strictEqual( - new LocalDate(2015, 12, 13).toString(), - "2015-12-13", - ); - assert.strictEqual( - new LocalDate(101, 12, 14).toString(), - "0101-12-14", - ); - assert.strictEqual( - new LocalDate(-100, 11, 6).toString(), - "-0100-11-06", - ); - }); - }); - describe("#fromBuffer() and #toBuffer()", function () { - it("should encode and decode a LocalDate", function () { - const value = new LocalDate(2010, 8, 5); - const encoded = value.toBuffer(); - const decoded = LocalDate.fromBuffer(encoded); - assert.strictEqual(decoded.toString(), value.toString()); - assert.ok(decoded.equals(value)); - assert.ok(value.equals(decoded)); - }); - }); - describe("#fromString()", function () { - it("should parse the string representation as yyyy-mm-dd", function () { - [ - ["1200-12-30", 1200, 12, 30], - ["1-1-1", 1, 1, 1], - ["21-2-1", 21, 2, 1], - ["-21-2-1", -21, 2, 1], - ["2010-4-29", 2010, 4, 29], - ["-199-06-30", -199, 6, 30], - ["1201-04-03", 1201, 4, 3], - ["-1201-04-03", -1201, 4, 3], - ["0-1-1", 0, 1, 1], - ].forEach(function (item) { - const value = LocalDate.fromString(item[0]); - assert.strictEqual(value.year, item[1]); - assert.strictEqual(value.month, item[2]); - assert.strictEqual(value.day, item[3]); - }); - }); - it("should parse the string representation as since epoch days", function () { - [ - ["0", "1970-01-01"], - ["1", "1970-01-02"], - ["2147483647", "2147483647"], - ["-2147483648", "-2147483648"], - ["-719162", "0001-01-01"], - ].forEach(function (item) { - const value = LocalDate.fromString(item[0]); - assert.strictEqual(value.toString(), item[1]); - }); - }); - it("should throw when string representation is invalid", function () { - ["", "1880-1", "1880-1-z", undefined, null, " "].forEach( - function (value) { - assert.throws(function () { - LocalDate.fromString(value); - }); - }, - ); - }); - }); - }); - describe("ResultStream", function () { it("should be readable as soon as it has data", function (done) { const buf = []; diff --git a/test/unit/local-date-tests.js b/test/unit/local-date-tests.js new file mode 100644 index 00000000..67204b51 --- /dev/null +++ b/test/unit/local-date-tests.js @@ -0,0 +1,178 @@ +"use strict"; +const assert = require("assert"); +const types = require("../../lib/types"); +const LocalDate = types.LocalDate; + +describe("LocalDate", function () { + const LocalDate = types.LocalDate; + describe("new LocalDate", function () { + it("should refuse to create LocalDate from invalid values.", function () { + assert.throws(() => new types.LocalDate(), Error); + assert.throws(() => new types.LocalDate(undefined), Error); + // Outside of ES5 Date range. + assert.throws(() => new types.LocalDate(-271821, 4, 19), Error); + assert.throws(() => new types.LocalDate(275760, 9, 14), Error); + // Outside of LocalDate range. + assert.throws(() => new types.LocalDate(-2147483649), Error); + assert.throws(() => new types.LocalDate(2147483648), Error); + // Incorrect values for the date. + assert.throws(() => new types.LocalDate(2024, 13, 14), Error); + assert.throws(() => new types.LocalDate(2024, 10, 83), Error); + assert.throws(() => new types.LocalDate(2024, 4, 31), Error); + assert.throws(() => new types.LocalDate(2023, 2, 29), Error); + }); + }); + describe("#toString()", function () { + it("should return the string in the form of yyyy-mm-dd", function () { + assert.strictEqual( + new LocalDate(2015, 2, 1).toString(), + "2015-02-01", + ); + assert.strictEqual( + new LocalDate(2015, 12, 13).toString(), + "2015-12-13", + ); + assert.strictEqual( + new LocalDate(101, 12, 14).toString(), + "0101-12-14", + ); + assert.strictEqual( + new LocalDate(-100, 11, 6).toString(), + "-0100-11-06", + ); + }); + }); + describe("#fromBuffer() and #toBuffer()", function () { + it("should encode and decode a LocalDate", function () { + const value = new LocalDate(2010, 8, 5); + const encoded = value.toBuffer(); + const decoded = LocalDate.fromBuffer(encoded); + assert.strictEqual(decoded.toString(), value.toString()); + assert.ok(decoded.equals(value)); + assert.ok(value.equals(decoded)); + }); + }); + describe("number of days since the epoch (member value)", function () { + it("should return the correct number of days from the epoch", function () { + [ + ["1970-01-01", 0], + ["1970-01-02", 1], + ["2024-12-09", 20066], + ["21-2-1", -711826], + ["-21-2-1", -727167], + ["2010-4-29", 14728], + ["-199-06-30", -792031], + ["1201-04-03", -280779], + ["-1201-04-03", -1158092], + ["0-1-1", -719528], + ["1200-12-30", -280873], + ["3096-12-31", 411628], + ["2024-02-29", 19782], + ["2024-03-01", 19783], + ].forEach(function (item) { + const value = LocalDate.fromString(item[0]); + assert.strictEqual(value.value, item[1]); + }); + }); + }); + describe("#fromString()", function () { + it("should parse the string representation as yyyy-mm-dd", function () { + [ + ["1200-12-30", 1200, 12, 30], + ["1-1-1", 1, 1, 1], + ["21-2-1", 21, 2, 1], + ["-21-2-1", -21, 2, 1], + ["2010-4-29", 2010, 4, 29], + ["-199-06-30", -199, 6, 30], + ["1201-04-03", 1201, 4, 3], + ["-1201-04-03", -1201, 4, 3], + ["0-1-1", 0, 1, 1], + ].forEach(function (item) { + const value = LocalDate.fromString(item[0]); + assert.strictEqual(value.year, item[1]); + assert.strictEqual(value.month, item[2]); + assert.strictEqual(value.day, item[3]); + }); + }); + it("should parse the string representation as since epoch days", function () { + [ + ["0", "1970-01-01"], + ["1", "1970-01-02"], + ["2147483647", "2147483647"], + ["-2147483648", "-2147483648"], + ["-719162", "0001-01-01"], + ].forEach(function (item) { + const value = LocalDate.fromString(item[0]); + assert.strictEqual(value.toString(), item[1]); + }); + }); + it("should throw when string representation is invalid", function () { + ["", "1880-1", "1880-1-z", undefined, null, " "].forEach( + function (value) { + assert.throws(function () { + LocalDate.fromString(value); + }); + }, + ); + }); + }); + describe("setters", function () { + it("should throw errors", function () { + const val = LocalDate.fromString("2024-11-29"); + assert.throws( + function () { + val.year += 2; + }, + { + name: "SyntaxError", + message: "LocalDate year is read-only", + }, + ); + assert.throws( + function () { + val.month += 1; + }, + { + name: "SyntaxError", + message: "LocalDate month is read-only", + }, + ); + assert.throws( + function () { + val.day += 3; + }, + { + name: "SyntaxError", + message: "LocalDate day is read-only", + }, + ); + assert.throws( + function () { + val._value += 7; + }, + { + name: "SyntaxError", + message: "LocalDate _value is read-only", + }, + ); + assert.throws( + function () { + val.value += 4; + }, + { + name: "SyntaxError", + message: "LocalDate value is read-only", + }, + ); + assert.throws( + function () { + val.date = new Date(); + }, + { + name: "SyntaxError", + message: "LocalDate date is read-only", + }, + ); + }); + }); +}); From 37c4ee8612f172138819c5c5d667b647e1329d44 Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Wed, 27 Nov 2024 10:32:19 +0100 Subject: [PATCH 10/52] Add implementation of LocalDate in Rust Add implementation of Rust object with constracor and Cql getter/setter. Add some custom functions for date operations. --- Cargo.toml | 1 + src/types/local_date.rs | 215 ++++++++++++++++++++++++++++++++++++++++ src/types/mod.rs | 1 + 3 files changed, 217 insertions(+) create mode 100644 src/types/local_date.rs diff --git a/Cargo.toml b/Cargo.toml index 077d4fac..17ca53d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ scylla = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "v0 tokio = { version = "1.34", features = ["full"] } futures = "0.3" uuid = "1" +thiserror = "2.0.12" [build-dependencies] diff --git a/src/types/local_date.rs b/src/types/local_date.rs new file mode 100644 index 00000000..87effa66 --- /dev/null +++ b/src/types/local_date.rs @@ -0,0 +1,215 @@ +use crate::utils::js_error; +use scylla::frame::value::CqlDate; +use thiserror::Error; + +// Max and min date range of the Date class in JS. +const MAX_JS_DATE: i32 = 100_000_000; +const MIN_JS_DATE: i32 = -MAX_JS_DATE; + +// Number of leap years up to 1970. +const LEAP_YEAR_1970: i32 = 477; + +// Number of days to the beginning of each month. +const DAY_IN_MONTH: [i32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; + +/// LocalDateWrapper holds two data representations - value and day, month, year. +/// The class in JS has getters for both representations. +/// When a new LocalDateWrapper instance is created, the second representation is calculated. +#[napi] +pub struct LocalDateWrapper { + /// wrapper for number of days from 01.01.1970 + pub value: i32, + pub date: Option, + /// value can be represented as Date class in JS + pub in_date: bool, +} + +#[napi] +impl LocalDateWrapper { + /// Create a new object from the day, month and year. + #[napi] + pub fn new(day: i8, month: i8, year: i32) -> napi::Result { + let date = Ymd::new(year, month, day)?; + + let value = date.to_days(); + + Ok(LocalDateWrapper { + date: Some(date), + value, + in_date: (MIN_JS_DATE..=MAX_JS_DATE).contains(&value), + }) + } + + /// Create a new object from number of days since 01.01.1970. + #[napi] + pub fn new_day(value: i32) -> napi::Result { + let date = Ymd::from_days(value.into()); + Ok(LocalDateWrapper { + value, + date, + in_date: (MIN_JS_DATE..=MAX_JS_DATE).contains(&value), + }) + } + + pub fn get_cql_date(&self) -> CqlDate { + CqlDate(((1 << 31) + self.value) as u32) + } + + pub fn from_cql_date(date: CqlDate) -> Self { + let value: i32 = date.0 as i32 - (1 << 31); + let date = Ymd::from_days(value.into()); + LocalDateWrapper { + value, + date, + in_date: (MIN_JS_DATE..=MAX_JS_DATE).contains(&value), + } + } +} + +/// Checks whether the year is leap year. +fn is_leap_year(year: i32) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} + +fn number_leap_years(n: i32) -> i32 { + n / 4 - n / 100 + n / 400 +} + +#[napi(object)] +#[derive(Clone, PartialEq, Debug)] +pub struct Ymd { + pub year: i32, + pub month: i8, + pub day: i8, +} + +impl Ymd { + /// Create a new Ymd object + fn new(year: i32, month: i8, day: i8) -> Result { + if !(1..=12).contains(&month) { + return Err(DateInvalid::Month); + } + + if !(day >= 1 && day <= Self::days_in_month(month, year)) { + return Err(DateInvalid::Day); + } + + Ok(Ymd { year, month, day }) + } + + /// Counts the number of days since 01.01.1970. + fn to_days(&self) -> i32 { + let mut total_days = 0; + + let number_day = DAY_IN_MONTH[self.month as usize - 1] // number of days from 1 January + + if is_leap_year(self.year) && self.month > 2 { + 1 // add 29 February + } + else { 0 } + + self.day as i32 + - 1; + + if self.year >= 1970 { + total_days += (self.year - 1970) * 365 + + number_leap_years(self.year - 1) - LEAP_YEAR_1970 // number of leap years + + number_day; + } else { + total_days -= if is_leap_year(self.year) { 366 } else { 365 } - number_day; + if self.year < 0 { + total_days -= (1970 - self.year - 1) * 365 + + number_leap_years((self.year + 1).abs()) + + LEAP_YEAR_1970 + + 1; // year 0 is leap year + } else { + total_days -= (1970 - self.year - 1) * 365 + LEAP_YEAR_1970 + - number_leap_years(self.year + 1); + } + } + total_days + } + + /// Create a Ymd from the number of days since 01.01.1970. + fn from_days(mut n: i64) -> Option { + if !(MIN_JS_DATE..=MAX_JS_DATE).contains(&(n as i32)) { + None + } else { + let mut year: i32 = 1970; + while n.abs() >= 365 { + // Find the number of years in n days. + let k = (n / 365) as i32; + // The year 0 is leap year. + if year > 0 && year + k < 0 { + n += 1; + } else if year < 0 && year + k > 0 { + n -= 1; + } + + year += k; + + // Converting years into days and counting the number of leap years in this range. + n -= (k * 365 - number_leap_years(year - k - 1) + number_leap_years(year - 1)) + as i64; + } + + if n < 0 { + // If the remaining number of days is negative, change the year and count the complement. + year -= 1; + n += if is_leap_year(year) { 366 } else { 365 }; + } + + // Special handling of leap February. + if is_leap_year(year) && n >= 60 { + n -= 1; + } else if is_leap_year(year) && n > 31 { + return Some(Ymd { + year, + month: 2, + day: (n - 30) as i8, + }); + } + + let q = DAY_IN_MONTH // Find month. + .iter() + .enumerate() + .filter(|&(_, &x)| i64::from(x) <= n) + .max_by_key(|&(_, &x)| x) + .map(|(index, &_value)| index + 1) + .unwrap(); + + Some(Ymd { + year, + month: q as i8, + day: (n - DAY_IN_MONTH[q - 1] as i64) as i8 + 1, + }) + } + } + + /// Returns the number of days in a month. + fn days_in_month(month: i8, year: i32) -> i8 { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 => { + if is_leap_year(year) { + 29 + } else { + 28 + } + } + _ => 0, + } + } +} +#[derive(Error, Debug)] +enum DateInvalid { + #[error("Invalid month")] + Month, + #[error("Invalid number of day")] + Day, +} + +impl From for napi::Error { + fn from(value: DateInvalid) -> Self { + js_error(value) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 88ac272d..d6cde121 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,6 @@ pub mod duration; pub mod inet; +pub mod local_date; pub mod local_time; pub mod time_uuid; pub mod type_wrappers; From eb47053db8c5b41ebf5871276ef16715f36063cb Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Wed, 27 Nov 2024 22:19:56 +0100 Subject: [PATCH 11/52] Add support for rust objects in JS files Add getters and setters. Improved existing functions to use the rust object. --- lib/types/local-date.js | 202 ++++++++++++++++++++++++++-------------- 1 file changed, 134 insertions(+), 68 deletions(-) diff --git a/lib/types/local-date.js b/lib/types/local-date.js index 70b2e621..e089270d 100644 --- a/lib/types/local-date.js +++ b/lib/types/local-date.js @@ -1,9 +1,16 @@ "use strict"; const utils = require("../utils"); +const rust = require("../../index"); /** @module types */ /** - * Maximum number of days from 01.01.1970 supported by the class (i32::MAX in Rust).. + * 2^31 days before unix epoch is -5877641-06-23. This is the first day that can be represented by this class. + * @const + */ +const dateCenter = Math.pow(2, 31); + +/** + * Maximum number of days from 01.01.1970 supported by the class (i32::MAX in Rust). * @const */ const maxDay = 2147483647; @@ -18,8 +25,6 @@ const minDay = -2147483648; * @const */ const millisecondsPerDay = 86400000; - -const dateCenter = Math.pow(2, 31); /** * A date without a time-zone in the ISO-8601 calendar system, such as 2010-08-05. * @@ -36,6 +41,12 @@ const dateCenter = Math.pow(2, 31); * @property {Date} date The date representation if falls within a range of an ES5 data type, otherwise an invalid date. */ class LocalDate { + /** + * @type {rust.LocalTimeWrapper} + * @private + */ + #inner; + /** * Creates a new instance of LocalDate. * @param {Number} year The year or days since epoch. If days since epoch, month and day should not be provided. @@ -49,61 +60,108 @@ class LocalDate { typeof month === "number" && typeof day === "number" ) { - // Use setUTCFullYear as if there is a 2 digit year, Date.UTC() assumes - // that is the 20th century. - this.date = new Date(); - this.date.setUTCHours(0, 0, 0, 0); - this.date.setUTCFullYear(year, month - 1, day); - if (isNaN(this.date.getTime())) { + // It will throw an error if the values are wrong. + this.#inner = rust.LocalDateWrapper.new(day, month, year); + if (!this.#inner.inDate) { + throw new Error("You must provide a valid year, month and day"); + } + } else if ( + typeof year === "number" && + typeof month === "undefined" && + typeof day === "undefined" + ) { + // In days since epoch. + if (year < minDay || year > maxDay) { throw new Error( - util.format( - "%d-%d-%d does not form a valid ES5 date!", - year, - month, - day, - ), + `You must provide a valid value for days since epoch (${minDay} <= value <= ${maxDay}).`, ); } - } else if (typeof month === "undefined" && typeof day === "undefined") { - if (typeof year === "number") { - // In days since epoch. - if (year < minDay || year > maxDay) { - throw new Error( - `You must provide a valid value for days since epoch (${minDay} <= value <= ${maxDay}).`, - ); - } - this.date = new Date(year * millisecondsPerDay); - } - } - - if (typeof this.date === "undefined") { + this.#inner = rust.LocalDateWrapper.newDay(year); + } else { throw new Error("You must provide a valid year, month and day"); } + } + + /** + * A number representing the year. May return NaN if cannot be represented as a Date. + * @readonly + * @type {Number} + */ + get year() { + return this.#inner.inDate ? this.#inner.date.year : NaN; + } + + set year(_) { + throw new SyntaxError("LocalDate year is read-only"); + } + + /** + * A number between 1 and 12 inclusive representing the month. + * May return NaN if cannot be represented as a Date. + * @readonly + * @type {Number} + */ + get month() { + return this.#inner.inDate ? this.#inner.date.month : NaN; + } + + set month(_) { + throw new SyntaxError("LocalDate month is read-only"); + } + + /** + * A number between 1 and the number of days in the given month of the given year (value up to 31). + * May return NaN if cannot be represented as a Date. + * @readonly + * @type {Number} + */ + get day() { + return this.#inner.inDate ? this.#inner.date.day : NaN; + } + + set day(_) { + throw new SyntaxError("LocalDate day is read-only"); + } + + /** + * If date cannot be represented yet given a valid days since epoch, track it internally. + * @readonly + * @deprecated This member is in Datastax documentation, but it seems to not be exposed in the API. + * Additionally we added a new class member: ``value`` that always returns days since epoch regardless of the date. + * @type {Number} + */ + get _value() { + return this.#inner.inDate ? null : this.#inner.value; + } + + set _value(_) { + throw new SyntaxError("LocalDate _value is read-only"); + } - /** - * If date cannot be represented yet given a valid days since epoch, track - * it internally. - */ - this._value = isNaN(this.date.getTime()) ? year : null; + /** + * Always valid amount of days since epoch. + * @readonly + * @type {Number} + */ + get value() { + return this.#inner.value; + } - /** - * A number representing the year. May return NaN if cannot be represented as - * a Date. - * @type Number - */ - this.year = this.date.getUTCFullYear(); - /** - * A number between 1 and 12 inclusive representing the month. May return - * NaN if cannot be represented as a Date. - * @type Number - */ - this.month = this.date.getUTCMonth() + 1; - /** - * A number between 1 and the number of days in the given month of the given year (value up to 31). - * May return NaN if cannot be represented as a Date. - * @type Number - */ - this.day = this.date.getUTCDate(); + set value(_) { + throw new SyntaxError("LocalDate value is read-only"); + } + + /** + * Date object represent this date. + * @readonly + * @type {Date} + */ + get date() { + return new Date(this.#inner.value * millisecondsPerDay); + } + + set date(_) { + throw new SyntaxError("LocalDate date is read-only"); } /** @@ -181,20 +239,12 @@ class LocalDate { * if the given one is greater. */ compare(other) { - const thisValue = isNaN(this.date.getTime()) - ? this._value * millisecondsPerDay - : this.date.getTime(); - const otherValue = isNaN(other.date.getTime()) - ? other._value * millisecondsPerDay - : other.date.getTime(); - const diff = thisValue - otherValue; - if (diff < 0) { - return -1; - } - if (diff > 0) { + if (this.value == other.value) { + return 0; + } else if (this.value > other.value) { return 1; } - return 0; + return -1; } /** @@ -205,6 +255,7 @@ class LocalDate { equals(other) { return other instanceof LocalDate && this.compare(other) === 0; } + /** * Provide the name of the constructor and the string representation * @returns {string} @@ -219,10 +270,7 @@ class LocalDate { */ toBuffer() { // days since unix epoch - const daysSinceEpoch = isNaN(this.date.getTime()) - ? this._value - : Math.floor(this.date.getTime() / millisecondsPerDay); - const value = daysSinceEpoch + dateCenter; + const value = this.#inner.value + dateCenter; const buf = utils.allocBufferUnsafe(4); buf.writeUInt32BE(value, 0); return buf; @@ -236,7 +284,7 @@ class LocalDate { toString() { let result; // if cannot be parsed as date, return days since epoch representation. - if (isNaN(this.date.getTime())) { + if (!this.#inner.inDate) { return this._value.toString(); } if (this.year < 0) { @@ -259,6 +307,24 @@ class LocalDate { toJSON() { return this.toString(); } + + /** + * Get LocalDate from rust object. + * @package + * @param {rust.LocalDateWrapper} arg + * @returns {LocalDate} + */ + static fromRust(arg) { + return new LocalDate(arg.value); + } + + /** + * @package + * @returns {rust.LocalDateWrapper} + */ + getInternal() { + return this.#inner; + } } module.exports = LocalDate; From 19b81375856abb37d8ec78545630f1f7430bb5d6 Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Thu, 28 Nov 2024 22:52:17 +0100 Subject: [PATCH 12/52] Implement LocalDate.toString() function in Rust Implement to_format() with Display trait. This function in JS called toString(). --- lib/types/local-date.js | 29 +------------------------- src/types/local_date.rs | 46 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/lib/types/local-date.js b/lib/types/local-date.js index e089270d..c9bfd20b 100644 --- a/lib/types/local-date.js +++ b/lib/types/local-date.js @@ -282,22 +282,7 @@ class LocalDate { * @returns {string} */ toString() { - let result; - // if cannot be parsed as date, return days since epoch representation. - if (!this.#inner.inDate) { - return this._value.toString(); - } - if (this.year < 0) { - result = "-" + fillZeros((this.year * -1).toString(), 4); - } else { - result = fillZeros(this.year.toString(), 4); - } - result += - "-" + - fillZeros(this.month.toString(), 2) + - "-" + - fillZeros(this.day.toString(), 2); - return result; + return this.#inner.toString(); } /** @@ -327,15 +312,3 @@ class LocalDate { } } module.exports = LocalDate; - -/** - * @param {string} value - * @param {Number} amount - * @private - */ -function fillZeros(value, amount) { - if (value.length >= amount) { - return value; - } - return utils.stringRepeat("0", amount - value.length) + value; -} diff --git a/src/types/local_date.rs b/src/types/local_date.rs index 87effa66..37a039f5 100644 --- a/src/types/local_date.rs +++ b/src/types/local_date.rs @@ -1,5 +1,9 @@ -use crate::utils::js_error; +use crate::utils::{js_error, CharCounter}; use scylla::frame::value::CqlDate; +use std::{ + cmp::max, + fmt::{self, Write}, +}; use thiserror::Error; // Max and min date range of the Date class in JS. @@ -51,6 +55,11 @@ impl LocalDateWrapper { }) } + #[napi(js_name = "toString")] + pub fn to_format(&self) -> String { + self.to_string() + } + pub fn get_cql_date(&self) -> CqlDate { CqlDate(((1 << 31) + self.value) as u32) } @@ -66,6 +75,41 @@ impl LocalDateWrapper { } } +impl fmt::Display for LocalDateWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.date { + Some(date) => { + if date.year < 0 { + write!(f, "-")?; + } + + let mut counter = CharCounter::new(); + write!(&mut counter, "{}", date.year.abs())?; + + for _ in 0..(max(4 - counter.count() as i8, 0)) { + write!(f, "0")?; + } + write!(f, "{}", date.year.abs())?; + + if date.month < 10 { + write!(f, "-0{}", date.month)?; + } else { + write!(f, "-{}", date.month)?; + } + if date.day < 10 { + write!(f, "-0{}", date.day)?; + } else { + write!(f, "-{}", date.day)?; + } + Ok(()) + } + None => { + write!(f, "{}", self.value) + } + } + } +} + /// Checks whether the year is leap year. fn is_leap_year(year: i32) -> bool { (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) From ee16725ac06f20c34eb0249d22e76eb8690327e1 Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Tue, 3 Dec 2024 15:14:29 +0100 Subject: [PATCH 13/52] Implement LocalDate.fromString() function in Rust Move the function logic to rust. --- Cargo.toml | 1 + lib/types/local-date.js | 21 ++-------------- src/types/local_date.rs | 54 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 17ca53d4..36e04dc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ scylla = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "v0 tokio = { version = "1.34", features = ["full"] } futures = "0.3" uuid = "1" +regex = "1.11.1" thiserror = "2.0.12" diff --git a/lib/types/local-date.js b/lib/types/local-date.js index c9bfd20b..7982c815 100644 --- a/lib/types/local-date.js +++ b/lib/types/local-date.js @@ -201,25 +201,8 @@ class LocalDate { * @returns {LocalDate} */ static fromString(value) { - const dashCount = (value.match(/-/g) || []).length; - if (dashCount >= 2) { - let multiplier = 1; - if (value[0] === "-") { - value = value.substring(1); - multiplier = -1; - } - const parts = value.split("-"); - return new LocalDate( - multiplier * parseInt(parts[0], 10), - parseInt(parts[1], 10), - parseInt(parts[2], 10), - ); - } - if (value.match(/^-?\d+$/)) { - // Parse as days since epoch. - return new LocalDate(parseInt(value, 10)); - } - throw new Error(`Invalid input ${value}.`); + let days = rust.LocalDateWrapper.fromString(value); + return new LocalDate(days); } /** diff --git a/src/types/local_date.rs b/src/types/local_date.rs index 37a039f5..c86d561b 100644 --- a/src/types/local_date.rs +++ b/src/types/local_date.rs @@ -1,5 +1,7 @@ use crate::utils::{js_error, CharCounter}; +use regex::Regex; use scylla::frame::value::CqlDate; +use std::sync::LazyLock; use std::{ cmp::max, fmt::{self, Write}, @@ -16,6 +18,12 @@ const LEAP_YEAR_1970: i32 = 477; // Number of days to the beginning of each month. const DAY_IN_MONTH: [i32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; +// based on https://stackoverflow.com/a/22061879 +static DATE_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^-?\d{1}\d*-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])$") + .expect("Invalid regex pattern") +}); + /// LocalDateWrapper holds two data representations - value and day, month, year. /// The class in JS has getters for both representations. /// When a new LocalDateWrapper instance is created, the second representation is calculated. @@ -73,6 +81,50 @@ impl LocalDateWrapper { in_date: (MIN_JS_DATE..=MAX_JS_DATE).contains(&value), } } + + /// Returns the number of days since 01.01.1970 based on a String representing the date. + #[napi] + pub fn from_string(value: String) -> napi::Result { + match value.chars().filter(|c| *c == '-').count() { + d if d < 2 => match value.parse::() { + Ok(val) => Ok(val), + Err(_) => Err(DateInvalid::Format.into()), + }, + 2 | 3 => { + if !DATE_REGEX.is_match(&value) { + return Err(DateInvalid::Format.into()); + } + + let lambda = |s: String| -> Result<(i32, i8, i8), DateInvalid> { + // From checking the regex and from removing the first '-', + // it is clear that the date string has three '-'. + let date = s.strip_prefix('-').unwrap_or(&s); + + let mut parts = date.split('-'); + let y = parts.next().and_then(|q| q.parse::().ok()); + let m = parts.next().and_then(|q| q.parse::().ok()); + let d = parts.next().and_then(|q| q.parse::().ok()); + let (Some(y), Some(m), Some(d)) = (y, m, d) else { + return Err(DateInvalid::Format); + }; + Ok((if s.starts_with('-') { -1 } else { 1 } * y, m, d)) + }; + + match lambda(value) { + Ok(s) => { + let date = Ymd { + year: s.0, + month: s.1, + day: s.2, + }; + Ok(date.to_days()) + } + Err(e) => Err(e.into()), + } + } + _ => Err(DateInvalid::Format.into()), + } + } } impl fmt::Display for LocalDateWrapper { @@ -250,6 +302,8 @@ enum DateInvalid { Month, #[error("Invalid number of day")] Day, + #[error("Invalid format of string")] + Format, } impl From for napi::Error { From 07185f43a57a223f144c68b1dcfe726c2e6ac55a Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Tue, 3 Dec 2024 19:27:48 +0100 Subject: [PATCH 14/52] Add tests for LocalDate conversion Between rust and JS and value into QueryParameterWrapper. --- lib/types/cql-utils.js | 9 ++ lib/types/results-wrapper.js | 3 + src/request.rs | 11 +- src/result.rs | 11 +- src/tests/result_tests.rs | 9 +- src/types/local_date.rs | 199 +++++++++++++++++++++++++++ test/unit/cql-value-wrapper-tests.js | 13 ++ test/unit/local-date-tests.js | 1 - 8 files changed, 251 insertions(+), 5 deletions(-) diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index 67b31f38..938a97c0 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -7,6 +7,7 @@ const { ResponseError } = require("../errors"); const TimeUuid = require("./time-uuid"); const Uuid = require("./uuid"); const Duration = require("./duration"); +const LocalDate = require("./local-date"); const LocalTime = require("./local-time"); const InetAddress = require("./inet-address"); @@ -89,6 +90,14 @@ function getWrapped(type, value) { return rust.QueryParameterWrapper.fromBoolean(value); case rust.CqlType.Counter: return rust.QueryParameterWrapper.fromCounter(BigInt(value)); + case rust.CqlType.Date: + if (!(value instanceof LocalDate)) + throw new TypeError( + "Expected LocalDate type to parse into Cql Date", + ); + return rust.QueryParameterWrapper.fromLocalDate( + value.getInternal(), + ); case rust.CqlType.Double: return rust.QueryParameterWrapper.fromDouble(value); case rust.CqlType.Duration: diff --git a/lib/types/results-wrapper.js b/lib/types/results-wrapper.js index 4052f172..0cbed90e 100644 --- a/lib/types/results-wrapper.js +++ b/lib/types/results-wrapper.js @@ -6,6 +6,7 @@ const TimeUuid = require("./time-uuid"); const Duration = require("./duration"); const LocalTime = require("./local-time"); const InetAddress = require("./inet-address"); +const LocalDate = require("./local-date"); const { bigintToLong } = require("../new-utils"); const Row = require("./row"); @@ -29,6 +30,8 @@ function getCqlObject(field) { return field.getBoolean(); case rust.CqlType.Counter: return field.getCounter(); + case rust.CqlType.Date: + return LocalDate.fromRust(field.getLocalDate()); case rust.CqlType.Double: return field.getDouble(); case rust.CqlType.Duration: diff --git a/src/request.rs b/src/request.rs index 930f1d42..3436b204 100644 --- a/src/request.rs +++ b/src/request.rs @@ -10,8 +10,8 @@ use scylla::{ use crate::{ result::map_column_type_to_complex_type, types::{ - duration::DurationWrapper, inet::InetAddressWrapper, local_time::LocalTimeWrapper, - type_wrappers::ComplexType, uuid::UuidWrapper, + duration::DurationWrapper, inet::InetAddressWrapper, local_date::LocalDateWrapper, + local_time::LocalTimeWrapper, type_wrappers::ComplexType, uuid::UuidWrapper, }, utils::{bigint_to_i64, js_error}, }; @@ -68,6 +68,13 @@ impl QueryParameterWrapper { }) } + #[napi] + pub fn from_local_date(val: &LocalDateWrapper) -> QueryParameterWrapper { + QueryParameterWrapper { + parameter: CqlValue::Date(val.get_cql_date()), + } + } + #[napi] pub fn from_double(val: f64) -> QueryParameterWrapper { QueryParameterWrapper { diff --git a/src/result.rs b/src/result.rs index b5f37ed0..3dd03203 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,5 +1,6 @@ use crate::{ types::{ + local_date::LocalDateWrapper, time_uuid::TimeUuidWrapper, type_wrappers::{ComplexType, CqlType}, uuid::UuidWrapper, @@ -165,7 +166,7 @@ impl CqlValueWrapper { CqlValue::Blob(_) => CqlType::Blob, CqlValue::Counter(_) => CqlType::Counter, CqlValue::Decimal(_) => CqlType::Decimal, // NOI - CqlValue::Date(_) => CqlType::Date, // NOI + CqlValue::Date(_) => CqlType::Date, CqlValue::Double(_) => CqlType::Double, CqlValue::Duration(_) => CqlType::Duration, CqlValue::Empty => CqlType::Empty, @@ -235,6 +236,14 @@ impl CqlValueWrapper { } } + #[napi] + pub fn get_local_date(&self) -> napi::Result { + match self.inner.as_cql_date() { + Some(r) => Ok(LocalDateWrapper::from_cql_date(r)), + None => Err(Self::generic_error("local_date")), + } + } + #[napi] pub fn get_double(&self) -> napi::Result { match self.inner.as_double() { diff --git a/src/tests/result_tests.rs b/src/tests/result_tests.rs index d7529740..a60d152b 100644 --- a/src/tests/result_tests.rs +++ b/src/tests/result_tests.rs @@ -5,7 +5,7 @@ use std::{ use scylla::frame::{ response::result::CqlValue, - value::{Counter, CqlDuration, CqlTime, CqlTimestamp, CqlTimeuuid}, + value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp, CqlTimeuuid}, }; use crate::result::CqlValueWrapper; @@ -46,6 +46,13 @@ pub fn tests_get_cql_wrapper_counter() -> CqlValueWrapper { CqlValueWrapper { inner: element } } +#[napi] +/// Test function returning sample CqlValueWrapper with CqlTime type +pub fn tests_get_cql_wrapper_date() -> CqlValueWrapper { + let element = CqlValue::Date(CqlDate((1 << 31) + 7)); + CqlValueWrapper { inner: element } +} + #[napi] /// Test function returning sample CqlValueWrapper with Double type pub fn tests_get_cql_wrapper_double() -> CqlValueWrapper { diff --git a/src/types/local_date.rs b/src/types/local_date.rs index c86d561b..eebbec0b 100644 --- a/src/types/local_date.rs +++ b/src/types/local_date.rs @@ -311,3 +311,202 @@ impl From for napi::Error { js_error(value) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_ymd_new() { + // correct date + assert!(Ymd::new(2025, 3, 1).is_ok()); + // invalid month + let invalid_month_list: [Result; 3] = [ + Ymd::new(2028, 30, 10), + Ymd::new(0, 0, 0), + Ymd::new(12, 15, 13), + ]; + for record in invalid_month_list { + assert!(record.is_err()); + assert!(matches!(record, Err(DateInvalid::Month))); + } + + // invalid day + let invalid_month_list: [Result; 3] = [ + Ymd::new(2025, 2, 29), + Ymd::new(0, 1, 0), + Ymd::new(2137, 12, 37), + ]; + for record in invalid_month_list { + assert!(record.is_err()); + assert!(matches!(record, Err(DateInvalid::Day))); + } + } + + #[test] + fn test_ymd_days() { + // Values from previous logic in JS. + let tests: [(Ymd, i32); 19] = [ + // simple examples + ( + Ymd { + year: 1970, + month: 1, + day: 1, + }, + 0, + ), + ( + Ymd { + year: 2025, + month: 3, + day: 1, + }, + 20148, + ), + ( + Ymd { + year: 1975, + month: 11, + day: 8, + }, + 2137, + ), + ( + Ymd { + year: 150196, + month: 12, + day: 26, + }, + 54138795, + ), + ( + Ymd { + year: 2005, + month: 4, + day: 2, + }, + 12875, + ), + ( + Ymd { + year: 44444, + month: 3, + day: 1, + }, + 15513370, + ), + ( + Ymd { + year: 21, + month: 2, + day: 1, + }, + -711826, + ), + // Negative number of days. + ( + Ymd { + year: -3881, + month: 2, + day: 4, + }, + -2137000, + ), + ( + Ymd { + year: 1969, + month: 12, + day: 31, + }, + -1, + ), + ( + Ymd { + year: 0, + month: 1, + day: 1, + }, + -719528, + ), + ( + Ymd { + year: 1968, + month: 9, + day: 27, + }, + -461, + ), + ( + Ymd { + year: 1964, + month: 2, + day: 25, + }, + -2137, + ), + ( + Ymd { + year: 404, + month: 5, + day: 3, + }, + -571847, + ), + ( + Ymd { + year: 944, + month: 2, + day: 2, + }, + -374707, + ), + // 29.02 before the epoch (negative and positive year) and after the epoch. + ( + Ymd { + year: -4, + month: 2, + day: 29, + }, + -720930, + ), + ( + Ymd { + year: 4, + month: 2, + day: 29, + }, + -718008, + ), + ( + Ymd { + year: 404, + month: 2, + day: 29, + }, + -571911, + ), + ( + Ymd { + year: 2044, + month: 2, + day: 29, + }, + 27087, + ), + ( + Ymd { + year: 2048, + month: 2, + day: 29, + }, + 28548, + ), + ]; + + for test in tests { + assert_eq!(test.0.to_days(), test.1); + assert_eq!(Ymd::from_days(test.1.into()), Some(test.0)); + } + } +} diff --git a/test/unit/cql-value-wrapper-tests.js b/test/unit/cql-value-wrapper-tests.js index dd268219..5efd00c9 100644 --- a/test/unit/cql-value-wrapper-tests.js +++ b/test/unit/cql-value-wrapper-tests.js @@ -8,6 +8,7 @@ const Duration = require("../../lib/types/duration"); const LocalTime = require("../../lib/types/local-time"); const Long = require("long"); const InetAddress = require("../../lib/types/inet-address"); +const LocalDate = require("../../lib/types/local-date"); const maxI64 = BigInt("9223372036854775807"); const maxI32 = Number(2147483647); @@ -64,6 +65,18 @@ describe("Cql value wrapper", function () { assert.strictEqual(value, maxI64); }); + it("should get LocalDate type correctly from napi", function () { + let element = rust.testsGetCqlWrapperDate(); + let type = element.getType(); + assert.strictEqual(type, rust.CqlType.Date); + let value = getCqlObject(element); + assert.instanceOf(value, LocalDate); + /* Corresponding value: + let element = CqlValue::Date(CqlDate((1 << 31) + 7)); */ + let expectedLocalDate = new LocalDate(7); + assert.equal(value.equals(expectedLocalDate), true); + }); + it("should get double type correctly from napi", function () { let element = rust.testsGetCqlWrapperDouble(); let type = element.getType(); diff --git a/test/unit/local-date-tests.js b/test/unit/local-date-tests.js index 67204b51..be9a47df 100644 --- a/test/unit/local-date-tests.js +++ b/test/unit/local-date-tests.js @@ -4,7 +4,6 @@ const types = require("../../lib/types"); const LocalDate = types.LocalDate; describe("LocalDate", function () { - const LocalDate = types.LocalDate; describe("new LocalDate", function () { it("should refuse to create LocalDate from invalid values.", function () { assert.throws(() => new types.LocalDate(), Error); From 2b3bd6b1ed0148999613c1cbbc6d4a76f589ea42 Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Mon, 3 Mar 2025 22:17:13 +0100 Subject: [PATCH 15/52] Enable Integration test Enable `with date and time types` integration test. --- test/integration/supported/client-execute-tests.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/integration/supported/client-execute-tests.js b/test/integration/supported/client-execute-tests.js index 1f7d969f..dddcda5d 100644 --- a/test/integration/supported/client-execute-tests.js +++ b/test/integration/supported/client-execute-tests.js @@ -1676,9 +1676,7 @@ describe("Client @SERVER_API", function () { ); }); */ - // No support for those types - // TODO: Fix this test - /* describe("with date and time types", function () { + describe("with date and time types", function () { const LocalDate = types.LocalDate; const LocalTime = types.LocalTime; const insertQuery = @@ -1770,7 +1768,7 @@ describe("Client @SERVER_API", function () { ); }, ); - }); */ + }); // No support for used types // TODO: Fix this test From bfdb3834231795357f48e2da55b75c4a92de61ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sat, 16 Nov 2024 12:09:03 +0100 Subject: [PATCH 16/52] Reorganize rust request structure Move code related to request logic into separate folder. --- src/lib.rs | 2 +- src/requests/mod.rs | 2 ++ .../parameter_wrappers.rs} | 31 +++---------------- src/requests/request.rs | 21 +++++++++++++ src/session.rs | 5 ++- src/tests/request_values_tests.rs | 2 +- 6 files changed, 31 insertions(+), 32 deletions(-) create mode 100644 src/requests/mod.rs rename src/{request.rs => requests/parameter_wrappers.rs} (87%) create mode 100644 src/requests/request.rs diff --git a/src/lib.rs b/src/lib.rs index 809e4b0c..2bc33ab5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ extern crate napi_derive; // Link other files pub mod auth; pub mod options; -pub mod request; +pub mod requests; pub mod result; pub mod session; pub mod tests; diff --git a/src/requests/mod.rs b/src/requests/mod.rs new file mode 100644 index 00000000..97ab7e5e --- /dev/null +++ b/src/requests/mod.rs @@ -0,0 +1,2 @@ +pub mod parameter_wrappers; +pub mod request; diff --git a/src/request.rs b/src/requests/parameter_wrappers.rs similarity index 87% rename from src/request.rs rename to src/requests/parameter_wrappers.rs index 3436b204..861e694c 100644 --- a/src/request.rs +++ b/src/requests/parameter_wrappers.rs @@ -1,17 +1,13 @@ use napi::bindgen_prelude::{BigInt, Buffer}; -use scylla::{ - frame::{ - response::result::CqlValue, - value::{Counter, CqlTimestamp, CqlTimeuuid}, - }, - prepared_statement::PreparedStatement, +use scylla::frame::{ + response::result::CqlValue, + value::{Counter, CqlTimestamp, CqlTimeuuid}, }; use crate::{ - result::map_column_type_to_complex_type, types::{ duration::DurationWrapper, inet::InetAddressWrapper, local_date::LocalDateWrapper, - local_time::LocalTimeWrapper, type_wrappers::ComplexType, uuid::UuidWrapper, + local_time::LocalTimeWrapper, uuid::UuidWrapper, }, utils::{bigint_to_i64, js_error}, }; @@ -22,12 +18,6 @@ pub struct QueryParameterWrapper { pub(crate) parameter: CqlValue, } -#[napi] -/// Wrapper for struct representing Prepared statement to the database -pub struct PreparedStatementWrapper { - pub(crate) prepared: PreparedStatement, -} - #[napi] impl QueryParameterWrapper { #[napi] @@ -206,16 +196,3 @@ impl QueryParameterWrapper { .collect() } } - -#[napi] -impl PreparedStatementWrapper { - #[napi] - /// Get array of expected types for this prepared statement. - pub fn get_expected_types(&self) -> Vec { - self.prepared - .get_variable_col_specs() - .iter() - .map(|e| map_column_type_to_complex_type(e.typ())) - .collect() - } -} diff --git a/src/requests/request.rs b/src/requests/request.rs new file mode 100644 index 00000000..79895a18 --- /dev/null +++ b/src/requests/request.rs @@ -0,0 +1,21 @@ +use scylla::prepared_statement::PreparedStatement; + +use crate::{result::map_column_type_to_complex_type, types::type_wrappers::ComplexType}; + +#[napi] +pub struct PreparedStatementWrapper { + pub(crate) prepared: PreparedStatement, +} + +#[napi] +impl PreparedStatementWrapper { + #[napi] + /// Get array of expected types for this prepared statement. + pub fn get_expected_types(&self) -> Vec { + self.prepared + .get_variable_col_specs() + .iter() + .map(|e| map_column_type_to_complex_type(e.typ())) + .collect() + } +} diff --git a/src/session.rs b/src/session.rs index 65b00d79..61a06293 100644 --- a/src/session.rs +++ b/src/session.rs @@ -3,10 +3,9 @@ use scylla::{ SessionBuilder, }; +use crate::requests::parameter_wrappers::QueryParameterWrapper; use crate::{ - options, - request::{PreparedStatementWrapper, QueryParameterWrapper}, - result::QueryResultWrapper, + options, requests::request::PreparedStatementWrapper, result::QueryResultWrapper, utils::err_to_napi, }; diff --git a/src/tests/request_values_tests.rs b/src/tests/request_values_tests.rs index edee4c53..fa1d446b 100644 --- a/src/tests/request_values_tests.rs +++ b/src/tests/request_values_tests.rs @@ -9,7 +9,7 @@ use scylla::frame::{ }; use crate::{ - request::QueryParameterWrapper, + requests::parameter_wrappers::QueryParameterWrapper, types::type_wrappers::{ComplexType, CqlType}, }; From 8e9169b2eda0153801a6de0b834e147b2f08f15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sat, 16 Nov 2024 12:27:46 +0100 Subject: [PATCH 17/52] Add simple QueryOptionsWrapper Currently this objects keeps only some of the parameters exposed in the API. Remaining parameters will need to be added at a later time. (All skipped options were due to complexity of the expected type) --- lib/client.js | 14 +++++------ src/requests/request.rs | 51 +++++++++++++++++++++++++++++++++++++++++ src/session.rs | 8 ++++++- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/lib/client.js b/lib/client.js index 158acaa4..d0665799 100644 --- a/lib/client.js +++ b/lib/client.js @@ -47,13 +47,6 @@ const { parseParams } = require("./types/cql-utils.js"); * @property {Object} [customPayload] Key-value payload to be passed to the server. On the Cassandra side, * implementations of QueryHandler can use this data. * [TODO: Add support for this field] - * @property {string} [executeAs] The user or role name to act as when executing this statement. - * - * When set, it executes as a different user/role than the one currently authenticated (a.k.a. proxy execution). - * - * This feature is only available in DSE 5.1+. - * - * [TODO: Add support for this field Delete?] * @property {string|ExecutionProfile} [executionProfile] Name or instance of the [profile]{@link ExecutionProfile} to * be used for this execution. If not set, it will the use "default" execution profile. * [TODO: Add support for this field] @@ -814,10 +807,14 @@ class Client extends events.EventEmitter { */ async #executePromise(resolve, reject, query, params) { try { + let rustOptions = rust.QueryOptionsWrapper.emptyOptions(); let rusty; // Optimize: If we have no parameters we can skip preparation step if (!params) { - rusty = await this.rustClient.queryUnpagedNoValues(query); + rusty = await this.rustClient.queryUnpagedNoValues( + query, + rustOptions, + ); } // Otherwise prepare the statement to know the types else { @@ -826,6 +823,7 @@ class Client extends events.EventEmitter { rusty = await this.rustClient.executePreparedUnpaged( statement, parsedParams, + rustOptions, ); } resolve(new ResultSet(rusty)); diff --git a/src/requests/request.rs b/src/requests/request.rs index 79895a18..9b80f966 100644 --- a/src/requests/request.rs +++ b/src/requests/request.rs @@ -1,3 +1,4 @@ +use napi::bindgen_prelude::BigInt; use scylla::prepared_statement::PreparedStatement; use crate::{result::map_column_type_to_complex_type, types::type_wrappers::ComplexType}; @@ -7,6 +8,32 @@ pub struct PreparedStatementWrapper { pub(crate) prepared: PreparedStatement, } +#[napi] +pub struct QueryOptionsWrapper { + pub auto_page: Option, + pub capture_stack_trace: Option, + pub consistency: Option, + pub counter: Option, + // customPayload?: any; + // executionProfile?: string | ExecutionProfile; + pub fetch_size: Option, + // hints?: string[] | string[][]; + // host?: Host; + pub is_idempotent: Option, + pub keyspace: Option, + pub logged: Option, + // pageState?: Buffer | string; + pub prepare: Option, + pub read_timeout: Option, + // retry?: policies.retry.RetryPolicy; + pub routing_indexes: Option>, + // routingKey?: Buffer | Buffer[]; + pub routing_names: Option>, + pub serial_consistency: Option, + pub timestamp: Option, + pub trace_query: Option, +} + #[napi] impl PreparedStatementWrapper { #[napi] @@ -19,3 +46,27 @@ impl PreparedStatementWrapper { .collect() } } + +#[napi] +impl QueryOptionsWrapper { + #[napi] + pub fn empty_options() -> QueryOptionsWrapper { + QueryOptionsWrapper { + auto_page: None, + capture_stack_trace: None, + consistency: None, + counter: None, + fetch_size: None, + is_idempotent: None, + keyspace: None, + logged: None, + prepare: None, + read_timeout: None, + routing_indexes: None, + routing_names: None, + serial_consistency: None, + timestamp: None, + trace_query: None, + } + } +} diff --git a/src/session.rs b/src/session.rs index 61a06293..3a695ae8 100644 --- a/src/session.rs +++ b/src/session.rs @@ -4,6 +4,7 @@ use scylla::{ }; use crate::requests::parameter_wrappers::QueryParameterWrapper; +use crate::requests::request::QueryOptionsWrapper; use crate::{ options, requests::request::PreparedStatementWrapper, result::QueryResultWrapper, utils::err_to_napi, @@ -60,7 +61,11 @@ impl SessionWrapper { } #[napi] - pub async fn query_unpaged_no_values(&self, query: String) -> napi::Result { + pub async fn query_unpaged_no_values( + &self, + query: String, + _options: &QueryOptionsWrapper, + ) -> napi::Result { let query_result = self .internal .query_unpaged(query, &[]) @@ -99,6 +104,7 @@ impl SessionWrapper { &self, query: &PreparedStatementWrapper, params: Vec>, + _options: &QueryOptionsWrapper, ) -> napi::Result { let params_vec: Vec> = QueryParameterWrapper::extract_parameters(params); QueryResultWrapper::from_query( From 27d02049433cc9d883e18753b1bb0c367540a9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sat, 16 Nov 2024 12:56:47 +0100 Subject: [PATCH 18/52] Make use of some query options fields Add conversion function that takes query QueryOptionsWrapper and apply it to the prepared statement. This takes use of only some of the QueryOptionsWrapper fields. --- src/session.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/session.rs b/src/session.rs index 3a695ae8..ef7d77a1 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,13 +1,16 @@ +use scylla::prepared_statement::PreparedStatement; +use scylla::statement::{Consistency, SerialConsistency}; use scylla::{ batch::Batch, frame::response::result::CqlValue, transport::SelfIdentity, Session, SessionBuilder, }; +use crate::options; use crate::requests::parameter_wrappers::QueryParameterWrapper; use crate::requests::request::QueryOptionsWrapper; +use crate::utils::{bigint_to_i64, js_error}; use crate::{ - options, requests::request::PreparedStatementWrapper, result::QueryResultWrapper, - utils::err_to_napi, + requests::request::PreparedStatementWrapper, result::QueryResultWrapper, utils::err_to_napi, }; #[napi] @@ -104,12 +107,13 @@ impl SessionWrapper { &self, query: &PreparedStatementWrapper, params: Vec>, - _options: &QueryOptionsWrapper, + options: &QueryOptionsWrapper, ) -> napi::Result { let params_vec: Vec> = QueryParameterWrapper::extract_parameters(params); + let query = apply_options(query.prepared.clone(), options)?; QueryResultWrapper::from_query( self.internal - .execute_unpaged(&query.prepared, params_vec) + .execute_unpaged(&query, params_vec) .await .map_err(err_to_napi)?, ) @@ -143,6 +147,51 @@ pub fn create_batch(queries: Vec<&PreparedStatementWrapper>) -> BatchWrapper { BatchWrapper { inner: batch } } +fn apply_options( + mut prepared: PreparedStatement, + options: &QueryOptionsWrapper, +) -> napi::Result { + if let Some(o) = options.consistency { + prepared.set_consistency( + Consistency::try_from(o) + .map_err(|_| js_error(format!("Unknown consistency value: {o}")))?, + ); + } + + if let Some(o) = options.serial_consistency { + prepared.set_serial_consistency(Some( + SerialConsistency::try_from(o) + .map_err(|_| js_error(format!("Unknown serial consistency value: {o}")))?, + )); + } + + if let Some(o) = options.is_idempotent { + prepared.set_is_idempotent(o); + } + // TODO: Update it and check all edge-cases: + // https://github.com/scylladb-zpp-2024-javascript-driver/scylladb-javascript-driver/pull/92#discussion_r1864461799 + // Currently there is no support for paging, so there is no need for this option + /* if let Some(o) = options.fetch_size { + if o.is_negative() { + return Err(js_error("fetch size cannot be negative")); + } + query.set_page_size(o); + } */ + if let Some(o) = &options.timestamp { + prepared.set_timestamp(Some(bigint_to_i64( + o.clone(), + "Timestamp cannot overflow i64", + )?)); + } + // TODO: Update it to allow collection of information from traced query + // Currently it's just passing the value, but not able to access any tracing information + if let Some(o) = options.trace_query { + prepared.set_tracing(o); + } + + Ok(prepared) +} + fn get_self_identity(options: &SessionOptions) -> SelfIdentity<'static> { let mut self_identity = SelfIdentity::new(); self_identity.set_custom_driver_name(options::DEFAULT_DRIVER_NAME); From 4d765318918b6b1728225708bc354396543023e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sat, 16 Nov 2024 20:48:27 +0100 Subject: [PATCH 19/52] Add query options parsing in js Converts js queryOptions into queryOptionsWrapper --- lib/client.js | 137 +++---------------------------------- lib/query-options.js | 156 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 128 deletions(-) create mode 100644 lib/query-options.js diff --git a/lib/client.js b/lib/client.js index d0665799..f7c1979a 100644 --- a/lib/client.js +++ b/lib/client.js @@ -18,126 +18,7 @@ const promiseUtils = require("./promise-utils"); const rust = require("../index"); const ResultSet = require("./types/result-set.js"); const { parseParams } = require("./types/cql-utils.js"); - -/** - * Query options - * @typedef {Object} QueryOptions - * [TODO: Add support for this field] - * @property {boolean} [autoPage] Determines if the driver must retrieve the following result pages automatically. - * - * This setting is only considered by the [Client#eachRow()]{@link Client#eachRow} method. For more information, - * check the - * [paging results documentation]{@link https://docs.datastax.com/en/developer/nodejs-driver/latest/features/paging/}. - * - * [TODO: Add support for this field] - * @property {boolean} [captureStackTrace] Determines if the stack trace before the query execution should be - * maintained. - * - * Useful for debugging purposes, it should be set to ``false`` under production environment as it adds an - * unnecessary overhead to each execution. - * - * Default: false. - * [TODO: Add support for this field] - * @property {number} [consistency] [Consistency level]{@link module:types~consistencies}. - * - * Defaults to ``localOne`` for Apache Cassandra and DSE deployments. - * For DataStax Astra, it defaults to ``localQuorum``. - * - * [TODO: Add support for this field] - * @property {Object} [customPayload] Key-value payload to be passed to the server. On the Cassandra side, - * implementations of QueryHandler can use this data. - * [TODO: Add support for this field] - * @property {string|ExecutionProfile} [executionProfile] Name or instance of the [profile]{@link ExecutionProfile} to - * be used for this execution. If not set, it will the use "default" execution profile. - * [TODO: Add support for this field] - * @property {number} [fetchSize] Amount of rows to retrieve per page. - * [TODO: Add support for this field] - * @property {Array|Array} [hints] Type hints for parameters given in the query, ordered as for the parameters. - * - * For batch queries, an array of such arrays, ordered as with the queries in the batch. - * [TODO: Add support for this field] - * @property {Host} [host] The host that should handle the query. - * - * Use of this option is **heavily discouraged** and should only be used in the following cases: - * - * 1. Querying node-local tables, such as tables in the ``system`` and ``system_views`` keyspaces. - * 2. Applying a series of schema changes, where it may be advantageous to execute schema changes in sequence on the - * same node. - * - * Configuring a specific host causes the configured - * [LoadBalancingPolicy]{@link module:policies/loadBalancing~LoadBalancingPolicy} to be completely bypassed. - * However, if the load balancing policy dictates that the host is at a - * [distance of ignored]{@link module:types~distance} or there is no active connectivity to the host, the request will - * fail with a [NoHostAvailableError]{@link module:errors~NoHostAvailableError}. - * - * [TODO: Add support for this field] - * @property {boolean} [isIdempotent] Defines whether the query can be applied multiple times without changing the result - * beyond the initial application. - * - * The query execution idempotence can be used at [RetryPolicy]{@link module:policies/retry~RetryPolicy} level to - * determine if an statement can be retried in case of request error or write timeout. - * - * Default: ``false``. - * - * [TODO: Add support for this field] - * @property {string} [keyspace] Specifies the keyspace for the query. It is used for the following: - * - * 1. To indicate what keyspace the statement is applicable to (protocol V5+ only). This is useful when the - * query does not provide an explicit keyspace and you want to override the current {@link Client#keyspace}. - * 2. For query routing when the query operates on a different keyspace than the current {@link Client#keyspace}. - * - * [TODO: Add support for this field] - * @property {boolean} [logged] Determines if the batch should be written to the batchlog. Only valid for - * [Client#batch()]{@link Client#batch}, it will be ignored by other methods. Default: true. - * [TODO: Add support for this field] - * @property {boolean} [counter] Determines if its a counter batch. Only valid for - * [Client#batch()]{@link Client#batch}, it will be ignored by other methods. Default: false. - * [TODO: Add support for this field] - * @property {Buffer|string} [pageState] Buffer or string token representing the paging state. - * - * Useful for manual paging, if provided, the query will be executed starting from a given paging state. - * [TODO: Add support for this field] - * @property {boolean} [prepare] Determines if the query must be executed as a prepared statement. - * [TODO: Add support for this field] - * @property {number} [readTimeout] When defined, it overrides the default read timeout - * (``socketOptions.readTimeout``) in milliseconds for this execution per coordinator. - * - * Suitable for statements for which the coordinator may allow a longer server-side timeout, for example aggregation - * queries. - * - * A value of ``0`` disables client side read timeout for the execution. Default: ``undefined``. - * - * [TODO: Add support for this field] - * @property {RetryPolicy} [retry] Retry policy for the query. - * - * This property can be used to specify a different [retry policy]{@link module:policies/retry} to the one specified - * in the {@link ClientOptions}.policies. - * [TODO: Add support for this field] - * @property {Array} [routingIndexes] Index of the parameters that are part of the partition key to determine - * the routing. - * [TODO: Add support for this field] - * @property {Buffer|Array} [routingKey] Partition key(s) to determine which coordinator should be used for the query. - * [TODO: Add support for this field] - * @property {Array} [routingNames] Array of the parameters names that are part of the partition key to determine the - * routing. Only valid for non-prepared requests, it's recommended that you use the prepare flag instead. - * [TODO: Add support for this field] - * @property {number} [serialConsistency] Serial consistency is the consistency level for the serial phase of - * conditional updates. - * This option will be ignored for anything else that a conditional update/insert. - * [TODO: Add support for this field] - * @property {number|Long} [timestamp] The default timestamp for the query in microseconds from the unix epoch - * (00:00:00, January 1st, 1970). - * - * If provided, this will replace the server side assigned timestamp as default timestamp. - * - * Use [generateTimestamp()]{@link module:types~generateTimestamp} utility method to generate a valid timestamp - * based on a Date and microseconds parts. - * [TODO: Add support for this field] - * @property {boolean} [traceQuery] Enable query tracing for the execution. Use query tracing to diagnose performance - * problems related to query executions. Default: false. - * - * To retrieve trace, you can call [Metadata.getTrace()]{@link module:metadata~Metadata#getTrace} method. - */ +const queryOptions = require("./query-options.js"); /** * Represents a database client that maintains multiple connections to the cluster nodes, providing methods to @@ -330,7 +211,7 @@ class Client extends events.EventEmitter { * @param {string} query The query to execute. * @param {Array|Object} [params] Array of parameter values or an associative array (object) containing parameter names * as keys and its value. - * @param {QueryOptions} [options] The query options for the execution. + * @param {queryOptions.QueryOptions} [options] The query options for the execution. * @param {ResultCallback} [callback] Executes callback(err, result) when execution completed. When not defined, the * method will return a promise. * @example Promise-based API, using async/await @@ -393,7 +274,7 @@ class Client extends events.EventEmitter { * @param {string} query The query to execute * @param {Array|Object} [params] Array of parameter values or an associative array (object) containing parameter names * as keys and its value. - * @param {QueryOptions} [options] The query options. + * @param {queryOptions.QueryOptions} [options] The query options. * @param {function} rowCallback Executes ``rowCallback(n, row)`` per each row received, where n is the row * index and row is the current Row. * @param {function} [callback] Executes ``callback(err, result)`` after all rows have been received. @@ -486,7 +367,7 @@ class Client extends events.EventEmitter { * @param {string} query The query to prepare and execute. * @param {Array|Object} [params] Array of parameter values or an associative array (object) containing parameter names * as keys and its value - * @param {QueryOptions} [options] The query options. + * @param {queryOptions.QueryOptions} [options] The query options. * @param {function} [callback] executes callback(err) after all rows have been received or if there is an error * @returns {ResultStream} */ @@ -546,7 +427,7 @@ class Client extends events.EventEmitter { * * @param {Array.|Array.<{query, params}>} queries The queries to execute as an Array of strings or as an array * of object containing the query and params - * @param {QueryOptions} [options] The query options. + * @param {queryOptions.QueryOptions} [options] The query options. * @param {ResultCallback} [callback] Executes callback(err, result) when the batch was executed */ batch(queries, options, callback) { @@ -564,7 +445,7 @@ class Client extends events.EventEmitter { /** * Async-only version of {@link Client#batch()} . * @param {Array.|Array.<{query, params}>}queries - * @param {QueryOptions} options + * @param {queryOptions.QueryOptions} options * @returns {Promise} * @private */ @@ -778,7 +659,7 @@ class Client extends events.EventEmitter { } return new Promise((resolve, reject) => { - this.#executePromise(resolve, reject, query, params); + this.#executePromise(resolve, reject, query, params, execOptions); }); } @@ -805,9 +686,9 @@ class Client extends events.EventEmitter { * @param {string} query * @param {Array} params */ - async #executePromise(resolve, reject, query, params) { + async #executePromise(resolve, reject, query, params, execOptions) { try { - let rustOptions = rust.QueryOptionsWrapper.emptyOptions(); + let rustOptions = queryOptions.queryOptionsIntoWrapper(execOptions); let rusty; // Optimize: If we have no parameters we can skip preparation step if (!params) { diff --git a/lib/query-options.js b/lib/query-options.js new file mode 100644 index 00000000..6d89e4c6 --- /dev/null +++ b/lib/query-options.js @@ -0,0 +1,156 @@ +const Long = require("long"); +const rust = require("../index"); +const _execOptions = require("./execution-options"); +const { longToBigint } = require("./new-utils"); + +/** + * Query options + * @typedef {Object} QueryOptions + * [TODO: Add support for this field] + * @property {boolean} [autoPage] Determines if the driver must retrieve the following result pages automatically. + * + * This setting is only considered by the [Client#eachRow()]{@link Client#eachRow} method. For more information, + * check the + * [paging results documentation]{@link https://docs.datastax.com/en/developer/nodejs-driver/latest/features/paging/}. + * + * [TODO: Add support for this field] + * @property {boolean} [captureStackTrace] Determines if the stack trace before the query execution should be + * maintained. + * + * Useful for debugging purposes, it should be set to ``false`` under production environment as it adds an + * unnecessary overhead to each execution. + * + * Default: false. + * [TODO: Add support for this field] + * @property {number} [consistency] [Consistency level]{@link module:types~consistencies}. + * + * Defaults to ``localOne`` for Apache Cassandra and ScyllaDB deployments. + * + * [TODO: Test this field] + * @property {Object} [customPayload] Key-value payload to be passed to the server. On the Cassandra side, + * implementations of QueryHandler can use this data. + * [TODO: Add support for this field] + * @property {string|ExecutionProfile} [executionProfile] Name or instance of the [profile]{@link ExecutionProfile} to + * be used for this execution. If not set, it will the use "default" execution profile. + * [TODO: Add support for this field] + * @property {number} [fetchSize] Amount of rows to retrieve per page. + * [TODO: Add support for this field] + * @property {Array|Array} [hints] Type hints for parameters given in the query, ordered as for the parameters. + * + * For batch queries, an array of such arrays, ordered as with the queries in the batch. + * [TODO: Add support for this field] + * @property {Host} [host] The host that should handle the query. + * + * Use of this option is **heavily discouraged** and should only be used in the following cases: + * + * 1. Querying node-local tables, such as tables in the ``system`` and ``system_views`` keyspaces. + * 2. Applying a series of schema changes, where it may be advantageous to execute schema changes in sequence on the + * same node. + * + * Configuring a specific host causes the configured + * [LoadBalancingPolicy]{@link module:policies/loadBalancing~LoadBalancingPolicy} to be completely bypassed. + * However, if the load balancing policy dictates that the host is at a + * [distance of ignored]{@link module:types~distance} or there is no active connectivity to the host, the request will + * fail with a [NoHostAvailableError]{@link module:errors~NoHostAvailableError}. + * + * [TODO: Add support for this field] + * @property {boolean} [idempotent] Defines whether the query can be applied multiple times without changing the result + * beyond the initial application. + * + * The query execution idempotence can be used at [RetryPolicy]{@link module:policies/retry~RetryPolicy} level to + * determine if an statement can be retried in case of request error or write timeout. + * + * Default: ``false``. + * + * [TODO: Add support for this field] + * @property {string} [keyspace] Specifies the keyspace for the query. It is used for the following: + * + * 1. To indicate what keyspace the statement is applicable to (protocol V5+ only). This is useful when the + * query does not provide an explicit keyspace and you want to override the current {@link Client#keyspace}. + * 2. For query routing when the query operates on a different keyspace than the current {@link Client#keyspace}. + * + * [TODO: Add support for this field] + * @property {boolean} [logged] Determines if the batch should be written to the batchlog. Only valid for + * [Client#batch()]{@link Client#batch}, it will be ignored by other methods. Default: true. + * [TODO: Add support for this field] + * @property {boolean} [counter] Determines if its a counter batch. Only valid for + * [Client#batch()]{@link Client#batch}, it will be ignored by other methods. Default: false. + * [TODO: Add support for this field] + * @property {Buffer|string} [pageState] Buffer or string token representing the paging state. + * + * Useful for manual paging, if provided, the query will be executed starting from a given paging state. + * [TODO: Add support for this field] + * @property {boolean} [prepare] Determines if the query must be executed as a prepared statement. + * [TODO: Add support for this field] + * @property {number} [readTimeout] When defined, it overrides the default read timeout + * (``socketOptions.readTimeout``) in milliseconds for this execution per coordinator. + * + * Suitable for statements for which the coordinator may allow a longer server-side timeout, for example aggregation + * queries. + * + * A value of ``0`` disables client side read timeout for the execution. Default: ``undefined``. + * + * [TODO: Add support for this field] + * @property {RetryPolicy} [retry] Retry policy for the query. + * + * This property can be used to specify a different [retry policy]{@link module:policies/retry} to the one specified + * in the {@link ClientOptions}.policies. + * [TODO: Add support for this field] + * @property {Array} [routingIndexes] Index of the parameters that are part of the partition key to determine + * the routing. + * [TODO: Add support for this field] + * @property {Buffer|Array} [routingKey] Partition key(s) to determine which coordinator should be used for the query. + * [TODO: Add support for this field] + * @property {Array} [routingNames] Array of the parameters names that are part of the partition key to determine the + * routing. Only valid for non-prepared requests, it's recommended that you use the prepare flag instead. + * [TODO: Add support for this field] + * @property {number} [serialConsistency] Serial consistency is the consistency level for the serial phase of + * conditional updates. + * This option will be ignored for anything else that a conditional update/insert. + * [TODO: Add support for this field] + * @property {number|Long} [timestamp] The default timestamp for the query in microseconds from the unix epoch + * (00:00:00, January 1st, 1970). + * + * If provided, this will replace the server side assigned timestamp as default timestamp. + * + * Use [generateTimestamp()]{@link module:types~generateTimestamp} utility method to generate a valid timestamp + * based on a Date and microseconds parts. + * [TODO: Test this field] + * @property {boolean} [traceQuery] Enable query tracing for the execution. Use query tracing to diagnose performance + * problems related to query executions. Default: false. + * + * To retrieve trace, you can call [Metadata.getTrace()]{@link module:metadata~Metadata#getTrace} method. + */ + +/** + * Parses js query options into rust query options wrapper + * @param {_execOptions.ExecutionOptions} options + * @returns {rust.QueryOptionsWrapper} + * @package + */ +function queryOptionsIntoWrapper(options) { + let wrapper = rust.QueryOptionsWrapper.emptyOptions(); + + wrapper.autoPage = options.isAutoPage(); + wrapper.captureStackTrace = options.getCaptureStackTrace(); + wrapper.consistency = options.getConsistency(); + wrapper.counter = options.counter; + wrapper.fetchSize = options.getFetchSize(); + wrapper.isIdempotent = options.isIdempotent(); + wrapper.keyspace = options.keyspace; + wrapper.logged = options.logged; + wrapper.prepare = options.prepare; + wrapper.readTimeout = options.getReadTimeout(); + wrapper.routingIndexes = options.getRoutingIndexes(); + wrapper.routingNames = options.getRoutingNames(); + wrapper.serialConsistency = options.getSerialConsistency(); + let timestamp = options.getTimestamp(); + if (timestamp instanceof Long) timestamp = longToBigint(timestamp); + else if (timestamp) timestamp = BigInt(timestamp); + wrapper.timestamp = timestamp; + wrapper.traceQuery = options.traceQuery; + + return wrapper; +} + +module.exports.queryOptionsIntoWrapper = queryOptionsIntoWrapper; From 85aa2e07b5adaf6e4030596a669d7fc958a40376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sat, 16 Nov 2024 21:15:09 +0100 Subject: [PATCH 20/52] Update and enable some integration tests --- .../client-execute-prepared-tests.js | 6 ++-- .../supported/client-execute-tests.js | 30 +++++++------------ 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/test/integration/supported/client-execute-prepared-tests.js b/test/integration/supported/client-execute-prepared-tests.js index ad5e90a2..c14cb722 100644 --- a/test/integration/supported/client-execute-prepared-tests.js +++ b/test/integration/supported/client-execute-prepared-tests.js @@ -899,9 +899,7 @@ describe("Client @SERVER_API", function () { }); */ - // Protocol level timestamp implemented in #92 - // TODO: fix this test - /* vit("2.1", "should support protocol level timestamp", function (done) { + vit("2.1", "should support protocol level timestamp", function (done) { const client = setupInfo.client; const id = Uuid.random(); const timestamp = types.generateTimestamp(new Date(), 456); @@ -951,7 +949,7 @@ describe("Client @SERVER_API", function () { ], done, ); - }); */ + }); // No support for client keyspace option // TODO: fix this test diff --git a/test/integration/supported/client-execute-tests.js b/test/integration/supported/client-execute-tests.js index dddcda5d..b42a710b 100644 --- a/test/integration/supported/client-execute-tests.js +++ b/test/integration/supported/client-execute-tests.js @@ -543,10 +543,7 @@ describe("Client @SERVER_API", function () { }, ); }); - - // No support for consistency - // TODO: Fix this test - /* it("should accept localOne and localQuorum consistencies", function (done) { + it("should accept localOne and localQuorum consistencies", function (done) { const client = setupInfo.client; utils.series( [ @@ -569,9 +566,9 @@ describe("Client @SERVER_API", function () { ], done, ); - }); */ + }); - // No support for consistency + // No support for ExecutionProfile // TODO: Fix this test /* it("should use consistency level from profile and override profile when provided in query options", function (done) { const client = newInstance({ @@ -824,7 +821,7 @@ describe("Client @SERVER_API", function () { ); }); */ - // No support for consistency levels + // No support for ExecutionProfile consistency levels // TODO: Fix this test /* vit( "2.0", @@ -896,9 +893,7 @@ describe("Client @SERVER_API", function () { }, ); */ - // No support for protocol level timestamp - // TODO: Fix this test - /* vit("2.1", "should support protocol level timestamp", function (done) { + vit("2.1", "should support protocol level timestamp", function (done) { const client = setupInfo.client; const id = types.Uuid.random(); const timestamp = types.generateTimestamp(new Date(), 777); @@ -938,7 +933,7 @@ describe("Client @SERVER_API", function () { ], done, ); - }); */ + }); // No support for queryTrace flag // TODO: Fix this test @@ -1983,10 +1978,7 @@ describe("Client @SERVER_API", function () { }, ); }); */ - - // No support for consistency - // TODO: Fix this test - /* describe("with no callback specified", function () { + describe("with no callback specified", function () { vit( "2.0", "should return a promise with the result as a value", @@ -2024,7 +2016,8 @@ describe("Client @SERVER_API", function () { }); }, ); - it("should reject the promise when there is a syntax error", function () { + // Would require correct error throwing + /* it("should reject the promise when there is a syntax error", function () { const client = setupInfo.client; return client .connect() @@ -2037,9 +2030,8 @@ describe("Client @SERVER_API", function () { .catch(function (err) { helper.assertInstanceOf(err, errors.ResponseError); }); - }); - }); */ - + }); */ + }); vdescribe("2.0", "with lightweight transactions", function () { const client = setupInfo.client; const id = types.Uuid.random(); From f637bd2f2073bdfdd1fe94c365cc5a01d4c30453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Thu, 16 Jan 2025 22:41:04 +0100 Subject: [PATCH 21/52] Fix tests --- test/unit-not-supported/typescript/api-generation-test.ts | 1 - test/unit-not-supported/typescript/mapping-tests.ts | 2 +- test/unit-not-supported/typescript/metadata-tests.ts | 2 +- test/unit-not-supported/typescript/policy-tests.ts | 2 +- test/unit-not-supported/typescript/types-test.ts | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/test/unit-not-supported/typescript/api-generation-test.ts b/test/unit-not-supported/typescript/api-generation-test.ts index a3033377..83d6beeb 100644 --- a/test/unit-not-supported/typescript/api-generation-test.ts +++ b/test/unit-not-supported/typescript/api-generation-test.ts @@ -2,7 +2,6 @@ import { auth, concurrent, errors, - datastax, mapping, metadata, metrics, diff --git a/test/unit-not-supported/typescript/mapping-tests.ts b/test/unit-not-supported/typescript/mapping-tests.ts index 12fb3b91..58cdc6b2 100644 --- a/test/unit-not-supported/typescript/mapping-tests.ts +++ b/test/unit-not-supported/typescript/mapping-tests.ts @@ -1,4 +1,4 @@ -import { Client, mapping, types } from "../../../index"; +import { Client, mapping, types } from "../../../main"; import Mapper = mapping.Mapper; import ModelMapper = mapping.ModelMapper; import Uuid = types.Uuid; diff --git a/test/unit-not-supported/typescript/metadata-tests.ts b/test/unit-not-supported/typescript/metadata-tests.ts index 196af53a..4277b201 100644 --- a/test/unit-not-supported/typescript/metadata-tests.ts +++ b/test/unit-not-supported/typescript/metadata-tests.ts @@ -1,4 +1,4 @@ -import { Client, Host, metadata, types } from "../../../index"; +import { Client, Host, metadata, types } from "../../../main"; import TableMetadata = metadata.TableMetadata; import QueryTrace = metadata.QueryTrace; diff --git a/test/unit-not-supported/typescript/policy-tests.ts b/test/unit-not-supported/typescript/policy-tests.ts index 5d717182..66d673e0 100644 --- a/test/unit-not-supported/typescript/policy-tests.ts +++ b/test/unit-not-supported/typescript/policy-tests.ts @@ -1,4 +1,4 @@ -import { policies } from "../../../index"; +import { policies } from "../../../main"; import LoadBalancingPolicy = policies.loadBalancing.LoadBalancingPolicy; import TokenAwarePolicy = policies.loadBalancing.TokenAwarePolicy; import ReconnectionPolicy = policies.reconnection.ReconnectionPolicy; diff --git a/test/unit-not-supported/typescript/types-test.ts b/test/unit-not-supported/typescript/types-test.ts index b70541b8..df68d6d8 100644 --- a/test/unit-not-supported/typescript/types-test.ts +++ b/test/unit-not-supported/typescript/types-test.ts @@ -1,4 +1,4 @@ -import { types, Client } from "../../../index"; +import { types, Client } from "../../../main"; import Uuid = types.Uuid; import TimeUuid = types.TimeUuid; import Long = types.Long; From dcfa4f81a0e5847c4753acea6d382fa2efe9c4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Thu, 20 Feb 2025 17:57:35 +0100 Subject: [PATCH 22/52] Move UUID regex Move UUID regex, so it's accessible from other parts of the code --- lib/types/uuid.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/types/uuid.js b/lib/types/uuid.js index a31dd3b7..ef8120a2 100644 --- a/lib/types/uuid.js +++ b/lib/types/uuid.js @@ -6,18 +6,18 @@ const rust = require("../../index"); /** @module types */ -/** - * Used to check if the UUID is in a correct format - * Source: https://stackoverflow.com/a/6640851 - * Verified also with documentation of UUID library in Rust: https://docs.rs/uuid/latest/uuid/ - */ -const uuidRegex = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - /** * Represents an immutable universally unique identifier (UUID). A UUID represents a 128-bit value. */ class Uuid { + /** + * Used to check if the UUID is in a correct format + * Source: https://stackoverflow.com/a/6640851 + * Verified also with documentation of UUID library in Rust: https://docs.rs/uuid/latest/uuid/ + */ + static uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + /** * @type {rust.UuidWrapper} * @private @@ -56,7 +56,7 @@ class Uuid { * @returns {Uuid} */ static fromString(value) { - if (typeof value !== "string" || !uuidRegex.test(value)) { + if (typeof value !== "string" || !Uuid.uuidRegex.test(value)) { throw new Error( "Invalid string representation of Uuid, it should be in the 00000000-0000-0000-0000-000000000000 format", ); From 10e399d2e15931acf31607d681d3a09ff2cd57ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Thu, 20 Feb 2025 17:57:35 +0100 Subject: [PATCH 23/52] Adapt existing type guessing Make use of the existing type guessing done by datastax driver --- lib/types/type-guessing.js | 91 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 lib/types/type-guessing.js diff --git a/lib/types/type-guessing.js b/lib/types/type-guessing.js new file mode 100644 index 00000000..6e84c433 --- /dev/null +++ b/lib/types/type-guessing.js @@ -0,0 +1,91 @@ +const { errors } = require("../../main"); +const types = require("./index"); +const typeValues = types.dataTypes; +const Long = types.Long; +const Integer = types.Integer; +const BigDecimal = types.BigDecimal; + +function isTypedArray(arg) { + // The TypedArray superclass isn't available directly so to detect an instance of a TypedArray + // subclass we have to access the prototype of a concrete instance. There's nothing magical about + // Uint8Array here; we could just as easily use any of the other TypedArray subclasses. + return arg instanceof Object.getPrototypeOf(Uint8Array); +} + +/** + * Try to guess the Cassandra type to be stored, based on the javascript value type. + * This guessing is based on the guessing done by Datastax driver with minor alterations + * @param value + * @returns {{code: number, info: object}|null} + * @ignore + * @internal + */ +function guessType(value) { + let code = null; + let info = null; + const esTypeName = typeof value; + + if (esTypeName === "number") { + code = typeValues.double; + } else if (esTypeName === "string") { + code = typeValues.text; + if (value.length === 36 && types.Uuid.uuidRegex.test(value)) { + code = typeValues.uuid; + } + } else if (esTypeName === "boolean") { + code = typeValues.boolean; + } else if (value instanceof Buffer) { + code = typeValues.blob; + } else if (value instanceof Date) { + code = typeValues.timestamp; + } else if (value instanceof Long) { + code = typeValues.bigint; + } else if (value instanceof Integer) { + code = typeValues.varint; + } else if (value instanceof BigDecimal) { + code = typeValues.decimal; + } else if (value instanceof types.Uuid) { + code = typeValues.uuid; + } else if (value instanceof types.InetAddress) { + code = typeValues.inet; + } else if (value instanceof types.Tuple) { + code = typeValues.tuple; + info = []; + for (const element of value.elements) { + info.push(guessType(element)); + } + } else if (value instanceof types.LocalDate) { + code = typeValues.date; + } else if (value instanceof types.LocalTime) { + code = typeValues.time; + } else if (value instanceof types.Duration) { + code = typeValues.duration; + } + // Map JS TypedArrays onto vectors + else if (isTypedArray(value)) { + // TODO: Add support for typed arrays + throw new errors.DriverInternalError( + "No support for typed array guessing type", + ); + /* code = typeValues.Custom; + // DS TODO: another area that we have to generalize if we ever need to support vector subtypes other than float + info = buildParameterizedCustomType(customTypeNames.vector, [ + singleTypeNamesByDataType[typeValues.float], + value.length, + ]); */ + } else if (Array.isArray(value)) { + code = typeValues.list; + if (value.length == 0) { + throw new Error( + `TODO: Type guessing of empty array is not yet implemented`, + ); + } + info = guessType(value[0]); + } + + if (code === null) return null; + + return { code: code, info: info }; +} + +exports.guessType = guessType; From d679a7946e6b17ea41e251806d50535f3083cd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Thu, 20 Feb 2025 18:05:44 +0100 Subject: [PATCH 24/52] Organize tests for type guessing --- test/unit-not-supported/encoder-tests.js | 119 ---------------------- test/unit/type-guessing-tests.js | 123 +++++++++++++++++++++++ 2 files changed, 123 insertions(+), 119 deletions(-) create mode 100644 test/unit/type-guessing-tests.js diff --git a/test/unit-not-supported/encoder-tests.js b/test/unit-not-supported/encoder-tests.js index 90a976b0..d7cad1d8 100644 --- a/test/unit-not-supported/encoder-tests.js +++ b/test/unit-not-supported/encoder-tests.js @@ -22,125 +22,6 @@ const zeroLengthTypesSupported = new Set([ ]); describe("encoder", function () { - describe("Encoder.guessDataType()", function () { - it("should guess the native types", function () { - assertGuessed( - 1, - dataTypes.double, - "Guess type for an integer (double) number failed", - ); - assertGuessed( - 1.01, - dataTypes.double, - "Guess type for a double number failed", - ); - assertGuessed( - true, - dataTypes.boolean, - "Guess type for a boolean value failed", - ); - assertGuessed( - [1, 2, 3], - dataTypes.list, - "Guess type for an Array value failed", - ); - assertGuessed( - "a string", - dataTypes.text, - "Guess type for an string value failed", - ); - assertGuessed( - utils.allocBufferFromString("bip bop"), - dataTypes.blob, - "Guess type for a buffer value failed", - ); - assertGuessed( - new Date(), - dataTypes.timestamp, - "Guess type for a Date value failed", - ); - assertGuessed( - new types.Long(10), - dataTypes.bigint, - "Guess type for a Int 64 value failed", - ); - assertGuessed( - types.Uuid.random(), - dataTypes.uuid, - "Guess type for a UUID value failed", - ); - assertGuessed( - types.TimeUuid.now(), - dataTypes.uuid, - "Guess type for a TimeUuid value failed", - ); - assertGuessed( - types.TimeUuid.now().toString(), - dataTypes.uuid, - "Guess type for a string uuid value failed", - ); - assertGuessed( - types.timeuuid(), - dataTypes.uuid, - "Guess type for a Timeuuid value failed", - ); - assertGuessed( - types.Integer.fromNumber(1), - dataTypes.varint, - "Guess type for a varint value failed", - ); - assertGuessed( - types.BigDecimal.fromString("1.01"), - dataTypes.decimal, - "Guess type for a varint value failed", - ); - assertGuessed( - types.Integer.fromBuffer(utils.allocBufferFromArray([0xff])), - dataTypes.varint, - "Guess type for a varint value failed", - ); - assertGuessed( - new types.InetAddress( - utils.allocBufferFromArray([10, 10, 10, 2]), - ), - dataTypes.inet, - "Guess type for a inet value failed", - ); - assertGuessed( - new types.Tuple(1, 2, 3), - dataTypes.tuple, - "Guess type for a tuple value failed", - ); - assertGuessed( - new types.LocalDate(2010, 4, 29), - dataTypes.date, - "Guess type for a date value failed", - ); - assertGuessed( - new types.LocalTime(types.Long.fromString("6331999999911")), - dataTypes.time, - "Guess type for a time value failed", - ); - assertGuessed( - new Float32Array([1.2, 3.4, 5.6]), - dataTypes.custom, - "Guess type for a Float32 TypedArray value failed", - ); - assertGuessed({}, null, "Objects must not be guessed"); - }); - - function assertGuessed(value, expectedType, message) { - const type = Encoder.guessDataType(value); - if (type === null) { - if (expectedType !== null) { - assert.ok(false, "Type not guessed for value " + value); - } - return; - } - assert.strictEqual(type.code, expectedType, message + ": " + value); - } - }); - describe("Encoder.isTypedArray()", function () { it("should return true for TypedArray subclasses", function () { assert.ok(Encoder.isTypedArray(new Float32Array([]))); diff --git a/test/unit/type-guessing-tests.js b/test/unit/type-guessing-tests.js new file mode 100644 index 00000000..0629b753 --- /dev/null +++ b/test/unit/type-guessing-tests.js @@ -0,0 +1,123 @@ +const { assert } = require("chai"); +const rust = require("../../index"); +const utils = require("../../lib/utils"); +const types = require("../../lib/types"); +const typeGuessing = require("../../lib/types/type-guessing"); + +describe("Encoder.guessDataType()", function () { + it("should guess the native types", function () { + assertGuessed( + 1, + rust.CqlType.Double, + "Guess type for an integer (double) number failed", + ); + assertGuessed( + 1.01, + rust.CqlType.Double, + "Guess type for a double number failed", + ); + assertGuessed( + true, + rust.CqlType.Boolean, + "Guess type for a boolean value failed", + ); + assertGuessed( + [1, 2, 3], + rust.CqlType.List, + "Guess type for an Array value failed", + ); + assertGuessed( + "a string", + rust.CqlType.Text, + "Guess type for an string value failed", + ); + assertGuessed( + utils.allocBufferFromString("bip bop"), + rust.CqlType.Blob, + "Guess type for a buffer value failed", + ); + assertGuessed( + new Date(), + rust.CqlType.Timestamp, + "Guess type for a Date value failed", + ); + assertGuessed( + new types.Long(10), + rust.CqlType.BigInt, + "Guess type for a Int 64 value failed", + ); + assertGuessed( + types.Uuid.random(), + rust.CqlType.Uuid, + "Guess type for a UUID value failed", + ); + assertGuessed( + types.TimeUuid.now(), + rust.CqlType.Uuid, + "Guess type for a TimeUuid value failed", + ); + assertGuessed( + types.TimeUuid.now().toString(), + rust.CqlType.Uuid, + "Guess type for a string uuid value failed", + ); + assertGuessed( + types.timeuuid(), + rust.CqlType.Uuid, + "Guess type for a Timeuuid value failed", + ); + assertGuessed( + types.Integer.fromNumber(1), + rust.CqlType.Varint, + "Guess type for a varint value failed", + ); + assertGuessed( + types.BigDecimal.fromString("1.01"), + rust.CqlType.Decimal, + "Guess type for a varint value failed", + ); + assertGuessed( + types.Integer.fromBuffer(utils.allocBufferFromArray([0xff])), + rust.CqlType.Varint, + "Guess type for a varint value failed", + ); + assertGuessed( + new types.InetAddress(utils.allocBufferFromArray([10, 10, 10, 2])), + rust.CqlType.Inet, + "Guess type for a inet value failed", + ); + /* assertGuessed( + new types.Tuple(1, 2, 3), + rust.CqlType.Tuple, + "Guess type for a tuple value failed", + ); */ + assertGuessed( + new types.LocalDate(2010, 4, 29), + rust.CqlType.Date, + "Guess type for a date value failed", + ); + assertGuessed( + new types.LocalTime(types.Long.fromString("6331999999911")), + rust.CqlType.Time, + "Guess type for a time value failed", + ); + /* assertGuessed( + new Float32Array([1.2, 3.4, 5.6]), + rust.CqlType.Custom, + "Guess type for a Float32 TypedArray value failed", + ); */ + assertGuessed({}, null, "Objects must not be guessed"); + }); + + function assertGuessed(value, expectedType, message) { + let type = typeGuessing.guessType(value); + if (type === null) { + if (expectedType !== null) { + assert.ok(false, "Type not guessed for value " + value); + } + return; + } + type = rust.convertHint(type).baseType; + assert.strictEqual(type, expectedType, message + ": " + value); + } +}); From 1a2ab6e7c6ba77c92c9fcd8232de1125d41c1711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sun, 2 Mar 2025 10:33:14 +0100 Subject: [PATCH 25/52] Reorganize imports --- lib/client.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/client.js b/lib/client.js index f7c1979a..3c6629aa 100644 --- a/lib/client.js +++ b/lib/client.js @@ -12,7 +12,7 @@ const clientOptions = require("./client-options"); const ClientState = require("./metadata/client-state"); const description = require("../package.json").description; const { version } = require("../package.json"); -const { DefaultExecutionOptions } = require("./execution-options"); +const ExecOptions = require("./execution-options"); const ControlConnection = require("./control-connection"); const promiseUtils = require("./promise-utils"); const rust = require("../index"); @@ -243,7 +243,10 @@ class Client extends events.EventEmitter { } try { - const execOptions = DefaultExecutionOptions.create(options, this); + const execOptions = ExecOptions.DefaultExecutionOptions.create( + options, + this, + ); return promiseUtils.optionalCallback( this.#rustyExecute(query, params, execOptions), callback, @@ -308,7 +311,7 @@ class Client extends events.EventEmitter { let execOptions; try { - execOptions = DefaultExecutionOptions.create( + execOptions = ExecOptions.DefaultExecutionOptions.create( options, this, rowCallback, @@ -460,7 +463,10 @@ class Client extends events.EventEmitter { await this.#connect(); - const _execOptions = DefaultExecutionOptions.create(options, this); + const _execOptions = ExecOptions.DefaultExecutionOptions.create( + options, + this, + ); let preparedQueries = []; let parametersRows = []; @@ -634,7 +640,7 @@ class Client extends events.EventEmitter { * Wrapper for executing queries by rust driver * @param {string} query * @param {Array} params - * @param {ExecutionOptions} execOptions + * @param {ExecOptions.ExecutionOptions} execOptions * @returns {Promise} * @private */ From 731f7a92fb12c49aef5820a65e897cb094bd660d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Thu, 20 Feb 2025 20:29:59 +0100 Subject: [PATCH 26/52] Type guessing Update the queries, so they use type guessing. Fixes the driver so that no longer all queries are prepared. --- lib/client.js | 36 +++++++++++++++++++++++------------- lib/types/cql-utils.js | 39 +++++++++++++++++++++++++++++++++------ src/session.rs | 21 +++++++++++++++++++++ 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/lib/client.js b/lib/client.js index 3c6629aa..c36859ab 100644 --- a/lib/client.js +++ b/lib/client.js @@ -691,29 +691,39 @@ class Client extends events.EventEmitter { * @param {RejectCallback} reject * @param {string} query * @param {Array} params + * @param {ExecOptions.ExecutionOptions} execOptions */ async #executePromise(resolve, reject, query, params, execOptions) { try { let rustOptions = queryOptions.queryOptionsIntoWrapper(execOptions); - let rusty; - // Optimize: If we have no parameters we can skip preparation step - if (!params) { - rusty = await this.rustClient.queryUnpagedNoValues( - query, - rustOptions, - ); - } - // Otherwise prepare the statement to know the types - else { + let result; + if (execOptions.isPrepared()) { + // Execute prepared statement, as requested by the user let statement = await this.rustClient.prepareStatement(query); - let parsedParams = parseParams(statement, params); - rusty = await this.rustClient.executePreparedUnpaged( + let parsedParams = parseParams( + statement.getExpectedTypes(), + params, + ); + result = await this.rustClient.executePreparedUnpaged( statement, parsedParams, rustOptions, ); + } else if (!params) { + // Optimize: If we have no parameters we can skip preparation step + result = await this.rustClient.queryUnpagedNoValues( + query, + rustOptions, + ); + } else { + let expectedTypes = execOptions.getHints() || []; + let parsedParams = parseParams(expectedTypes, params, true); + result = await this.rustClient.queryUnpaged( + query, + parsedParams, + ); } - resolve(new ResultSet(rusty)); + resolve(new ResultSet(result)); } catch (e) { reject(e); } diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index 938a97c0..a5ef4b93 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -10,6 +10,7 @@ const Duration = require("./duration"); const LocalDate = require("./local-date"); const LocalTime = require("./local-time"); const InetAddress = require("./inet-address"); +const { guessType } = require("./type-guessing"); /** * Ensures the value isn't one of many ways to express null value @@ -187,21 +188,47 @@ function getWrapped(type, value) { /** * Parses array of params into rust objects according to preparedStatement expected types * Throws ResponseError when received different amount of parameters than expected - * @param {rust.PreparedStatementWrapper} preparedStatement + * + * If ``allowGuessing`` is true, then for each missing field of ``expectedTypes``, this function will try to guess a type. + * If the type cannot be guessed, error will be thrown. + * Field is missing if it is null, undefined (or if the ``expectedTypes`` list is to short) + * @param {Array} expectedTypes List of expected types. * @param {Array} params + * @param {boolean} [allowGuessing] * @returns {Array} */ -function parseParams(preparedStatement, params) { - let expectedTypes = preparedStatement.getExpectedTypes(); +function parseParams(expectedTypes, params, allowGuessing) { if (expectedTypes.length == 0 && !params) return []; - if (params.length != expectedTypes.length) + if (params.length != expectedTypes.length && !allowGuessing) { throw new ResponseError( types.responseErrorCodes.invalid, `Expected ${expectedTypes.length}, got ${params.length} parameters.`, ); + } let res = []; - for (let i = 0; i < expectedTypes.length; i++) { - res.push(getWrapped(expectedTypes[i], params[i])); + for (let i = 0; i < params.length; i++) { + if (params[i] === types.unset) { + // Rust driver works only with version >= 4 of CQL [Source?], so unset will always be supported. + // Currently we don't support unset values and we treat them as NULL values. + // TODO: Fix this + params[i] = null; + } + + if (params[i] === null) { + res.push(null); + continue; + } + + let type = expectedTypes[i]; + if (!type && allowGuessing) + type = rust.convertHint(guessType(params[i])); + if (!type) { + throw new TypeError( + "Target data type could not be guessed, you should use prepared statements for accurate type mapping. Value: " + + util.inspect(params[i]), + ); + } + res.push(getWrapped(type, params[i])); } return res; } diff --git a/src/session.rs b/src/session.rs index ef7d77a1..bd3ff858 100644 --- a/src/session.rs +++ b/src/session.rs @@ -77,6 +77,27 @@ impl SessionWrapper { QueryResultWrapper::from_query(query_result) } + /// Executes unprepared query. This assumes the types will be either guessed or provided by user. + /// + /// Returns a wrapper of the value provided by the rust driver + /// + /// All parameters need to be wrapped into QueryParameterWrapper keeping CqlValue of assumed correct type + /// If the provided types will not be correct, this query will fail. + #[napi] + pub async fn query_unpaged( + &self, + query: String, + params: Vec>, + ) -> napi::Result { + let params_vec: Vec> = QueryParameterWrapper::extract_parameters(params); + let query_result = self + .internal + .query_unpaged(query, params_vec) + .await + .map_err(err_to_napi)?; + QueryResultWrapper::from_query(query_result) + } + /// Prepares a statement through rust driver for a given session /// Return PreparedStatementWrapper that wraps object returned by the rust driver #[napi] From ded14ba9e22a98c646e46b5f840b5e222dae5ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Fri, 21 Feb 2025 18:49:31 +0100 Subject: [PATCH 27/52] Converting js object hints into Complex Type Converts all allowed types of parametr hints into Complex Type used by the driver. Currently this task is split between rust and JS, but it can be later fully moved to the rust part of the code.s Make use of napi object In function convert hint, instead of checking the types manually, move this job to NAPI-RS, by making use of napi objects. As info field can be either just a element, or array of elements, on the JS side I convert all non null objects into array of objects, and than convert back to single element on rust side. --- lib/client.js | 4 +- lib/types/cql-utils.js | 54 +++++++++++++++ src/types/mod.rs | 1 + src/types/type_hints.rs | 114 +++++++++++++++++++++++++++++++ test/unit/type-guessing-tests.js | 3 +- 5 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 src/types/type_hints.rs diff --git a/lib/client.js b/lib/client.js index c36859ab..431317f5 100644 --- a/lib/client.js +++ b/lib/client.js @@ -17,7 +17,7 @@ const ControlConnection = require("./control-connection"); const promiseUtils = require("./promise-utils"); const rust = require("../index"); const ResultSet = require("./types/result-set.js"); -const { parseParams } = require("./types/cql-utils.js"); +const { parseParams, convertHints } = require("./types/cql-utils.js"); const queryOptions = require("./query-options.js"); /** @@ -716,7 +716,7 @@ class Client extends events.EventEmitter { rustOptions, ); } else { - let expectedTypes = execOptions.getHints() || []; + let expectedTypes = convertHints(execOptions.getHints() || []); let parsedParams = parseParams(expectedTypes, params, true); result = await this.rustClient.queryUnpaged( query, diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index a5ef4b93..cfd900a7 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -233,7 +233,61 @@ function parseParams(expectedTypes, params, allowGuessing) { return res; } +/** + * Convert the hints from the formats allowed by the driver, to internal type representation + * @param {Array} hints + * @returns {Array} + */ +function convertHints(hints) { + let result = []; + + if (!Array.isArray(hints)) { + return []; + } + + for (const hint of hints) { + if (hint) { + /** @type {{code: Number, info: object}} */ + let objectHint = { + code: null, + info: null, + }; + if (typeof hint === "number") { + objectHint.code = hint; + } else if (typeof hint === "string") { + objectHint = types.dataTypes.getByName(hint); + } else if (typeof hint.code === "number") { + objectHint.code = hint.code; + objectHint.info = hint.info; + } + if (typeof objectHint.code !== "number") { + throw new TypeError( + "Type information not valid, only String and Number values are valid hints", + ); + } + result.push(rustConvertHint(objectHint)); + } else { + result.push(null); + } + } + return result; +} + +/** + * + * @param {null | object | Array} object + * @returns {rust.ComplexType | null} + */ +function rustConvertHint(object) { + if (object.info && !Array.isArray(object.info)) { + object.info = [object.info]; + } + return rust.convertHint(object); +} + module.exports.parseParams = parseParams; +module.exports.convertHints = convertHints; +module.exports.rustConvertHint = rustConvertHint; // For unit test usage module.exports.getWrapped = getWrapped; diff --git a/src/types/mod.rs b/src/types/mod.rs index d6cde121..fecc9c5a 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,5 +3,6 @@ pub mod inet; pub mod local_date; pub mod local_time; pub mod time_uuid; +pub mod type_hints; pub mod type_wrappers; pub mod uuid; diff --git a/src/types/type_hints.rs b/src/types/type_hints.rs new file mode 100644 index 00000000..8004096d --- /dev/null +++ b/src/types/type_hints.rs @@ -0,0 +1,114 @@ +use crate::utils::js_error; + +use super::type_wrappers::{ComplexType, CqlType}; + +// When converting from JS object, if a field is not provided (ex: {code: 3}), +// then the field becomes None. +// When a field is set to null (ex: {code: 3, info: null}), +// then the field becomes Some(None). +// As we want to support both absence of `info` field and `info` field set to null, +// we need to have nested Option in this object. +#[napi(object)] +pub struct TypeHint { + pub code: i32, + pub info: Option>>, +} + +/// Convert the number representing CQL type to the internal type, representing CQL type +fn type_code_to_cql_type(value: i32) -> Result { + Ok(match value { + 0x0000 => CqlType::Custom, + 0x0001 => CqlType::Ascii, + 0x0002 => CqlType::BigInt, + 0x0003 => CqlType::Blob, + 0x0004 => CqlType::Boolean, + 0x0005 => CqlType::Counter, + 0x0006 => CqlType::Decimal, + 0x0007 => CqlType::Double, + 0x0008 => CqlType::Float, + 0x0009 => CqlType::Int, + 0x000A => CqlType::Text, + 0x000B => CqlType::Timestamp, + 0x000C => CqlType::Uuid, + 0x000D => CqlType::Text, + 0x000E => CqlType::Varint, + 0x000F => CqlType::Timeuuid, + 0x0010 => CqlType::Inet, + 0x0011 => CqlType::Date, + 0x0012 => CqlType::Time, + 0x0013 => CqlType::SmallInt, + 0x0014 => CqlType::TinyInt, + 0x0015 => CqlType::Duration, + 0x0020 => CqlType::List, + 0x0021 => CqlType::Map, + 0x0022 => CqlType::Set, + 0x0030 => CqlType::UserDefinedType, + 0x0031 => CqlType::Tuple, + _ => { + return Err(js_error(format!( + "Number {} does not represent a known CQL type", + value + ))) + } + }) +} + +/// Convert single hint from the format expected by API of the driver +/// to the ``ComplexType`` that is used internally in the rust part of the code +#[napi] +pub fn convert_hint(hint: TypeHint) -> napi::Result { + let base_type = type_code_to_cql_type(hint.code)?; + let support_types = hint.info.flatten(); + + Ok(match base_type { + CqlType::List | CqlType::Set => { + // Ensure there is no value in info field, or there is exactly one support type + let mut support_types = support_types.unwrap_or_default(); + if support_types.len() > 1 { + return Err(js_error(format!( + "Invalid number of support types. Got {}, expected no more than one.", + support_types.len() + ))); + } + let support_type = convert_hint(support_types.pop().unwrap())?; + ComplexType::one_support(base_type, Some(support_type)) + } + CqlType::Map | CqlType::Tuple => { + let support_types: Vec = support_types.unwrap_or_default(); + + let support_types: Vec> = + support_types.into_iter().map(convert_hint).collect(); + + if support_types.iter().any(|e| e.is_err()) { + return Err(js_error("Failed to convert one of the subtypes")); + } + let mut support_types: Vec = + support_types.into_iter().filter_map(|e| e.ok()).collect(); + + match base_type { + CqlType::Map => { + if support_types.len() != 2 { + return Err(js_error(format!( + "Invalid number of support types. Got {}, expected 2", + support_types.len() + ))); + } + + // With the length check we assume, this values will always be Some, + // but we keep it as Option, as used Complex type constructor assumes Option + let second_support = support_types.pop(); + let first_support = support_types.pop(); + ComplexType::two_support(base_type, first_support, second_support) + } + // TODO: update it with tuple implementations + CqlType::Tuple => todo!(), + _ => unreachable!(), + } + } + CqlType::UserDefinedType => { + // TODO: update it with UDT implementation + todo!() + } + _ => ComplexType::simple_type(base_type), + }) +} diff --git a/test/unit/type-guessing-tests.js b/test/unit/type-guessing-tests.js index 0629b753..9d5dddcc 100644 --- a/test/unit/type-guessing-tests.js +++ b/test/unit/type-guessing-tests.js @@ -3,6 +3,7 @@ const rust = require("../../index"); const utils = require("../../lib/utils"); const types = require("../../lib/types"); const typeGuessing = require("../../lib/types/type-guessing"); +const { rustConvertHint } = require("../../lib/types/cql-utils"); describe("Encoder.guessDataType()", function () { it("should guess the native types", function () { @@ -117,7 +118,7 @@ describe("Encoder.guessDataType()", function () { } return; } - type = rust.convertHint(type).baseType; + type = rustConvertHint(type).baseType; assert.strictEqual(type, expectedType, message + ": " + value); } }); From c97d806d35168327cd6b89b5be3673d0617942f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sun, 23 Feb 2025 11:22:00 +0100 Subject: [PATCH 28/52] Disable broken test Why this test was previously working? Before this PR, all queries were treated as prepared queries. When this was changes, to not prepared(*), the error no longer relies on the js code, ensuring correct amount of parameters (see cql utils: parseParams) and now relies on the error returned by the database. With the lack of correct error mapping between rust driver and js driver, the error currently thrown is no longer Response error, but other error type: GenericFailure Error: Error#Serializing values failed: SerializationError: Failed to type check query arguments alloc::vec::Vec>: wrong column count: the statement operates on 2 columns, but the given rust type provides 1 --- .../supported/client-execute-tests.js | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test/integration/supported/client-execute-tests.js b/test/integration/supported/client-execute-tests.js index b42a710b..d753f4c5 100644 --- a/test/integration/supported/client-execute-tests.js +++ b/test/integration/supported/client-execute-tests.js @@ -58,6 +58,8 @@ describe("Client @SERVER_API", function () { }); }); */ + // Would require correct error conversion + // TODO: Fix this test context("with incorrect query parameters", () => { const client = setupInfo.client; const query = `INSERT INTO ${table} (id, bigint_sample) VALUES (?, ?)`; @@ -71,11 +73,13 @@ describe("Client @SERVER_API", function () { ], (params, next) => client.execute(query, params, (err) => { - helper.assertInstanceOf(err, errors.ResponseError); + // TODO: Ensure correct error is thrown + helper.assertInstanceOf(err, Error); + /* helper.assertInstanceOf(err, errors.ResponseError); assert.strictEqual( err.code, types.responseErrorCodes.invalid, - ); + ); */ next(); }), done, @@ -84,7 +88,7 @@ describe("Client @SERVER_API", function () { // Would require correct error throwing // TODO: Fix this test - /* it("should callback with error when the parameter types do not match", (done) => { + it("should callback with error when the parameter types do not match", (done) => { utils.eachSeries( [ [types.Uuid.random(), "a"], @@ -92,17 +96,19 @@ describe("Client @SERVER_API", function () { ], (params, next) => client.execute(query, params, (err) => { - helper.assertInstanceOf(err, errors.ResponseError); + // TODO: Ensure correct error is thrown + helper.assertInstanceOf(err, Error); + /* helper.assertInstanceOf(err, errors.ResponseError); assert.strictEqual( err.code, types.responseErrorCodes.invalid, - ); + ); */ next(); }), done, ); }); - + it("should callback with error when parameters can not be encoded", (done) => { utils.eachSeries( [ @@ -111,12 +117,14 @@ describe("Client @SERVER_API", function () { ], (params, next) => client.execute(query, params, (err) => { - helper.assertInstanceOf(err, TypeError); + // TODO: Ensure correct error is thrown + helper.assertInstanceOf(err, Error); + /* helper.assertInstanceOf(err, TypeError); */ next(); }), done, ); - }); */ + }); }); it("should callback with an empty Array instance as rows when not found", function (done) { From 4916e6c4f165cd4f6daab71882e92b385c68f27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sun, 23 Feb 2025 14:20:59 +0100 Subject: [PATCH 29/52] Unprepared batch queries Add ability to execute unprepared batch queries, and enables some integration tests for this feature. --- lib/client.js | 46 +++++++++++++------ src/session.rs | 11 ++++- .../supported/client-batch-tests.js | 23 +++++----- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/lib/client.js b/lib/client.js index 431317f5..049e982a 100644 --- a/lib/client.js +++ b/lib/client.js @@ -463,31 +463,49 @@ class Client extends events.EventEmitter { await this.#connect(); - const _execOptions = ExecOptions.DefaultExecutionOptions.create( + const execOptions = ExecOptions.DefaultExecutionOptions.create( options, this, ); - let preparedQueries = []; + let shouldBePrepared = execOptions.isPrepared(); + let allQueries = []; let parametersRows = []; + let hints = execOptions.getHints() || []; + for (let i = 0; i < queries.length; i++) { let element = queries[i]; - if (typeof element === "string") { - preparedQueries.push( - await this.rustClient.prepareStatement(element), - ); - parametersRows.push([]); + if (!element) { + throw new errors.ArgumentError(`Invalid query at index ${i}`); + } + /** + * @type {rust.PreparedStatementWrapper | string} + */ + let query = typeof element === "string" ? element : element.query; + let params = element.params || []; + let types; + + if (!query) { + throw new errors.ArgumentError(`Invalid query at index ${i}`); + } + + if (shouldBePrepared) { + query = await this.rustClient.prepareStatement(query); + types = query.getExpectedTypes(); } else { - let query = queries[i].query; - let params = queries[i].params; - let prepared = await this.rustClient.prepareStatement(query); - let parameters = parseParams(prepared, params); - preparedQueries.push(prepared); - parametersRows.push(parameters); + types = convertHints(hints[i] || []); + } + + if (params) { + params = parseParams(types, params, shouldBePrepared === false); } + allQueries.push(query); + parametersRows.push(params); } - let batch = rust.createBatch(preparedQueries); + let batch = shouldBePrepared + ? rust.createPreparedBatch(allQueries) + : rust.createUnpreparedBatch(allQueries); let wrappedResult = await this.rustClient.queryBatch( batch, parametersRows, diff --git a/src/session.rs b/src/session.rs index bd3ff858..324b7c4f 100644 --- a/src/session.rs +++ b/src/session.rs @@ -160,7 +160,7 @@ impl SessionWrapper { } #[napi] -pub fn create_batch(queries: Vec<&PreparedStatementWrapper>) -> BatchWrapper { +pub fn create_prepared_batch(queries: Vec<&PreparedStatementWrapper>) -> BatchWrapper { let mut batch: Batch = Default::default(); queries .iter() @@ -168,6 +168,15 @@ pub fn create_batch(queries: Vec<&PreparedStatementWrapper>) -> BatchWrapper { BatchWrapper { inner: batch } } +#[napi] +pub fn create_unprepared_batch(queries: Vec) -> BatchWrapper { + let mut batch: Batch = Default::default(); + queries + .into_iter() + .for_each(|q| batch.append_statement(q.as_str())); + BatchWrapper { inner: batch } +} + fn apply_options( mut prepared: PreparedStatement, options: &QueryOptionsWrapper, diff --git a/test/integration/supported/client-batch-tests.js b/test/integration/supported/client-batch-tests.js index 4f565b03..262e883e 100644 --- a/test/integration/supported/client-batch-tests.js +++ b/test/integration/supported/client-batch-tests.js @@ -266,9 +266,7 @@ describe("Client @SERVER_API", function () { done, ); }); - // Currently no support for hints - // TODO: Fix this test - /* vit("2.0", "should use hints when provided", function (done) { + vit("2.0", "should use hints when provided", function (done) { const client = newInstance(); const id1 = types.Uuid.random(); const id2 = types.Uuid.random(); @@ -285,7 +283,10 @@ describe("Client @SERVER_API", function () { "INSERT INTO %s (id, int_sample, bigint_sample) VALUES (?, ?, ?)", table1, ), - params: [id2, -1, -1], + // Would require full encoder support + // TODO: Fix this test + // params: [id2, -1, BigInt(-1)], + params: [id2, -1, BigInt(-1)], }, ]; const hints = [null, [null, "int", "bigint"]]; @@ -319,10 +320,9 @@ describe("Client @SERVER_API", function () { ); }, ); - }); */ - // Currently no support for hints - // TODO: Fix this test - /* vit( + }); + + vit( "2.0", "should callback in err when wrong hints are provided", function (done) { @@ -344,7 +344,7 @@ describe("Client @SERVER_API", function () { queries, { hints: {} }, function (err) { - //it should not fail, dismissed + // it should not fail, dismissed next(err); }, ); @@ -354,7 +354,7 @@ describe("Client @SERVER_API", function () { queries, { hints: [["uuid"]] }, function (err) { - //it should not fail + // it should not fail next(err); }, ); @@ -392,7 +392,8 @@ describe("Client @SERVER_API", function () { done, ); }, - ); */ + ); + // Currently no support for timestamps // TODO: Fix this test /* vit("2.1", "should support protocol level timestamp", function (done) { From 7e08caa87d743c6879d9ca528a91688b5c1b86bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 25 Feb 2025 17:47:19 +0100 Subject: [PATCH 30/52] Enable tests for type guessing --- .../supported/client-execute-tests.js | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/test/integration/supported/client-execute-tests.js b/test/integration/supported/client-execute-tests.js index d753f4c5..443d3dde 100644 --- a/test/integration/supported/client-execute-tests.js +++ b/test/integration/supported/client-execute-tests.js @@ -240,9 +240,7 @@ describe("Client @SERVER_API", function () { insertSelectTest(client, table, columns, values, null, done); }); */ - // No support for parameter hints - // TODO: Fix this test - /* vit( + vit( "2.0", "should use parameter hints as number for simple types", function (done) { @@ -263,7 +261,7 @@ describe("Client @SERVER_API", function () { ]; insertSelectTest(client, table, columns, values, hints, done); }, - ); + ); vit( "2.0", "should use parameter hints as string for simple types", @@ -274,8 +272,10 @@ describe("Client @SERVER_API", function () { const client = setupInfo.client; insertSelectTest(client, table, columns, values, hints, done); }, - ); - vit( + ); + // No support for partial hints + // TODO: Fix this test + /* vit( "2.0", "should use parameter hints as string for complex types partial", function (done) { @@ -290,7 +290,7 @@ describe("Client @SERVER_API", function () { const client = setupInfo.client; insertSelectTest(client, table, columns, values, hints, done); }, - ); + ); */ vit( "2.0", "should use parameter hints as string for complex types complete", @@ -312,8 +312,10 @@ describe("Client @SERVER_API", function () { const client = setupInfo.client; insertSelectTest(client, table, columns, values, hints, done); }, - ); - vit( + ); + // No support for map polyfills + // TODO: Fix this test + /* vit( "2.0", "should use parameter hints for custom map polyfills", function (done) { @@ -449,9 +451,9 @@ describe("Client @SERVER_API", function () { // No support for hint // TODO: Fix this test - /* vit( + vit( "2.0", - "should callback in err when wrong hints are provided", + "should callback with error when wrong hints are provided", function (done) { const client = setupInfo.client; const query = util.format( @@ -496,10 +498,12 @@ describe("Client @SERVER_API", function () { { hints: [[]] }, function (err) { helper.assertInstanceOf(err, Error); - helper.assertNotInstanceOf( + // Would require correct error throwing + // TODO: Fix this test + /* helper.assertNotInstanceOf( err, errors.NoHostAvailableError, - ); + ); */ next(); }, ); @@ -511,10 +515,12 @@ describe("Client @SERVER_API", function () { { hints: ["zzz", "mmmm"] }, function (err) { helper.assertInstanceOf(err, Error); - helper.assertNotInstanceOf( + // Would require correct error throwing + // TODO: Fix this test + /* helper.assertNotInstanceOf( err, errors.NoHostAvailableError, - ); + ); */ next(); }, ); @@ -523,7 +529,7 @@ describe("Client @SERVER_API", function () { done, ); }, - ); */ + ); vit("2.1", "should encode CONTAINS parameter", function (done) { const client = setupInfo.client; @@ -1587,8 +1593,8 @@ describe("Client @SERVER_API", function () { }, ); }); */ - // No support for rows length field - /* describe("with smallint and tinyint", function () { + + describe("with smallint and tinyint", function () { const sampleId = types.Uuid.random(); const insertQuery = "INSERT INTO tbl_smallints (id, smallint_sample, tinyint_sample, text_sample) VALUES (%s, %s, %s, %s)"; @@ -1677,7 +1683,7 @@ describe("Client @SERVER_API", function () { ); }, ); - }); */ + }); describe("with date and time types", function () { const LocalDate = types.LocalDate; From de8deb9a9aa4523941217d4be4ab5ee5030e3baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 25 Feb 2025 23:54:13 +0100 Subject: [PATCH 31/52] Add support for partial hints. Datastax driver allows to provide partial hints, for complex types: like map or list. This commit adds support for this feature, and enables appropriate integration tests. --- lib/types/cql-utils.js | 33 +++++++++++----- src/types/type_hints.rs | 38 +++++++++++-------- .../supported/client-execute-tests.js | 7 ++-- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index cfd900a7..dff94525 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -22,6 +22,22 @@ function ensureValue(val, message) { throw new TypeError(message); } +/** + * Guess the type, and if the type cannot be guessed, throw an error. + * @param {any} value + * @returns {rust.ComplexType} type guess, converted to ``ComplexType`` object + */ +function guessTypeChecked(value) { + let type = rustConvertHint(guessType(value)); + if (!type) { + throw new TypeError( + "Target data type could not be guessed, you should use prepared statements for accurate type mapping. Value: " + + util.inspect(value), + ); + } + return type; +} + /** * Wraps each of the elements into given subtype * @param {Array} values @@ -34,6 +50,8 @@ function encodeListLike(values, subtype) { `Not a valid list value, expected Array obtained ${util.inspect(values)}`, ); + if (!subtype) subtype = guessTypeChecked(values[0]); + let res = []; for (let i = 0; i < values.length; i++) { // This requirement is based on the datastax code @@ -54,6 +72,7 @@ function encodeListLike(values, subtype) { function encodeMap(value, parentType) { const keySubtype = parentType.getFirstSupportType(); const valueSubtype = parentType.getSecondSupportType(); + let res = []; for (const key in value) { @@ -61,7 +80,10 @@ function encodeMap(value, parentType) { continue; } const val = value[key]; - res.push([getWrapped(keySubtype, key), getWrapped(valueSubtype, val)]); + res.push([ + getWrapped(keySubtype || guessTypeChecked(key), key), + getWrapped(valueSubtype || guessTypeChecked(val), val), + ]); } return res; } @@ -220,14 +242,7 @@ function parseParams(expectedTypes, params, allowGuessing) { } let type = expectedTypes[i]; - if (!type && allowGuessing) - type = rust.convertHint(guessType(params[i])); - if (!type) { - throw new TypeError( - "Target data type could not be guessed, you should use prepared statements for accurate type mapping. Value: " + - util.inspect(params[i]), - ); - } + if (!type && allowGuessing) type = guessTypeChecked(params[i]); res.push(getWrapped(type, params[i])); } return res; diff --git a/src/types/type_hints.rs b/src/types/type_hints.rs index 8004096d..eae26772 100644 --- a/src/types/type_hints.rs +++ b/src/types/type_hints.rs @@ -56,11 +56,14 @@ fn type_code_to_cql_type(value: i32) -> Result { /// Convert single hint from the format expected by API of the driver /// to the ``ComplexType`` that is used internally in the rust part of the code #[napi] -pub fn convert_hint(hint: TypeHint) -> napi::Result { +pub fn convert_hint(hint: Option) -> napi::Result> { + let Some(hint) = hint else { + return Ok(None); + }; let base_type = type_code_to_cql_type(hint.code)?; let support_types = hint.info.flatten(); - Ok(match base_type { + Ok(Some(match base_type { CqlType::List | CqlType::Set => { // Ensure there is no value in info field, or there is exactly one support type let mut support_types = support_types.unwrap_or_default(); @@ -70,26 +73,25 @@ pub fn convert_hint(hint: TypeHint) -> napi::Result { support_types.len() ))); } - let support_type = convert_hint(support_types.pop().unwrap())?; - ComplexType::one_support(base_type, Some(support_type)) + let support_type = convert_hint(support_types.pop())?; + ComplexType::one_support(base_type, support_type) } CqlType::Map | CqlType::Tuple => { let support_types: Vec = support_types.unwrap_or_default(); - let support_types: Vec> = - support_types.into_iter().map(convert_hint).collect(); - - if support_types.iter().any(|e| e.is_err()) { - return Err(js_error("Failed to convert one of the subtypes")); - } - let mut support_types: Vec = - support_types.into_iter().filter_map(|e| e.ok()).collect(); + let mut support_types: Vec> = support_types + .into_iter() + .map(|e| convert_hint(Some(e))) + .collect::>, napi::Error>>() + .map_err(|err| { + js_error(format!("Failed to convert one of the subtypes: {}", err)) + })?; match base_type { CqlType::Map => { - if support_types.len() != 2 { + if ![0, 2].contains(&support_types.len()) { return Err(js_error(format!( - "Invalid number of support types. Got {}, expected 2", + "Invalid number of support types. Got {}, expected two or none", support_types.len() ))); } @@ -98,7 +100,11 @@ pub fn convert_hint(hint: TypeHint) -> napi::Result { // but we keep it as Option, as used Complex type constructor assumes Option let second_support = support_types.pop(); let first_support = support_types.pop(); - ComplexType::two_support(base_type, first_support, second_support) + ComplexType::two_support( + base_type, + first_support.flatten(), + second_support.flatten(), + ) } // TODO: update it with tuple implementations CqlType::Tuple => todo!(), @@ -110,5 +116,5 @@ pub fn convert_hint(hint: TypeHint) -> napi::Result { todo!() } _ => ComplexType::simple_type(base_type), - }) + })) } diff --git a/test/integration/supported/client-execute-tests.js b/test/integration/supported/client-execute-tests.js index 443d3dde..d009f0f7 100644 --- a/test/integration/supported/client-execute-tests.js +++ b/test/integration/supported/client-execute-tests.js @@ -273,9 +273,8 @@ describe("Client @SERVER_API", function () { insertSelectTest(client, table, columns, values, hints, done); }, ); - // No support for partial hints - // TODO: Fix this test - /* vit( + + vit( "2.0", "should use parameter hints as string for complex types partial", function (done) { @@ -290,7 +289,7 @@ describe("Client @SERVER_API", function () { const client = setupInfo.client; insertSelectTest(client, table, columns, values, hints, done); }, - ); */ + ); vit( "2.0", "should use parameter hints as string for complex types complete", From 499d3342b27042afa8034e2064f95d0a418aee01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sun, 2 Mar 2025 16:24:57 +0100 Subject: [PATCH 32/52] Rename struct fields Make the names more idiomatic --- src/session.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/session.rs b/src/session.rs index 324b7c4f..0beb4690 100644 --- a/src/session.rs +++ b/src/session.rs @@ -27,7 +27,7 @@ pub struct BatchWrapper { #[napi] pub struct SessionWrapper { - internal: Session, + inner: Session, } #[napi] @@ -52,15 +52,12 @@ impl SessionWrapper { .build() .await .map_err(err_to_napi)?; - Ok(SessionWrapper { internal: s }) + Ok(SessionWrapper { inner: s }) } #[napi] pub fn get_keyspace(&self) -> Option { - self.internal - .get_keyspace() - .as_deref() - .map(ToOwned::to_owned) + self.inner.get_keyspace().as_deref().map(ToOwned::to_owned) } #[napi] @@ -70,7 +67,7 @@ impl SessionWrapper { _options: &QueryOptionsWrapper, ) -> napi::Result { let query_result = self - .internal + .inner .query_unpaged(query, &[]) .await .map_err(err_to_napi)?; @@ -91,7 +88,7 @@ impl SessionWrapper { ) -> napi::Result { let params_vec: Vec> = QueryParameterWrapper::extract_parameters(params); let query_result = self - .internal + .inner .query_unpaged(query, params_vec) .await .map_err(err_to_napi)?; @@ -106,11 +103,7 @@ impl SessionWrapper { statement: String, ) -> napi::Result { Ok(PreparedStatementWrapper { - prepared: self - .internal - .prepare(statement) - .await - .map_err(err_to_napi)?, + prepared: self.inner.prepare(statement).await.map_err(err_to_napi)?, }) } @@ -133,7 +126,7 @@ impl SessionWrapper { let params_vec: Vec> = QueryParameterWrapper::extract_parameters(params); let query = apply_options(query.prepared.clone(), options)?; QueryResultWrapper::from_query( - self.internal + self.inner .execute_unpaged(&query, params_vec) .await .map_err(err_to_napi)?, @@ -151,7 +144,7 @@ impl SessionWrapper { .map(QueryParameterWrapper::extract_parameters) .collect(); QueryResultWrapper::from_query( - self.internal + self.inner .batch(&batch.inner, params_vec) .await .map_err(err_to_napi)?, From 8823790cee41d421784241b2bd0a85e2a212fa92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Fri, 7 Mar 2025 11:24:13 +0100 Subject: [PATCH 33/52] Disable incorrectly enabled test This test should not have been enabled in "Update and enable some integration tests" --- test/integration/supported/client-execute-tests.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/integration/supported/client-execute-tests.js b/test/integration/supported/client-execute-tests.js index d009f0f7..1fd24fed 100644 --- a/test/integration/supported/client-execute-tests.js +++ b/test/integration/supported/client-execute-tests.js @@ -906,7 +906,11 @@ describe("Client @SERVER_API", function () { }, ); */ - vit("2.1", "should support protocol level timestamp", function (done) { + // Incorrectly enabled test: enabled for prepared, instead of unprepared. + // Would require query options on unprepared queries + // https://github.com/scylladb-zpp-2024-javascript-driver/scylladb-javascript-driver/pull/92#discussion_r1977849708 + // TODO: Fix this test + /* vit("2.1", "should support protocol level timestamp", function (done) { const client = setupInfo.client; const id = types.Uuid.random(); const timestamp = types.generateTimestamp(new Date(), 777); @@ -946,7 +950,7 @@ describe("Client @SERVER_API", function () { ], done, ); - }); + }); */ // No support for queryTrace flag // TODO: Fix this test From 5878d37955dbbca109551148da9e3dfbaffb0847 Mon Sep 17 00:00:00 2001 From: Stapox35 Date: Tue, 11 Mar 2025 10:07:58 +0100 Subject: [PATCH 34/52] Fix casting values to CqlDate --- src/types/local_date.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/local_date.rs b/src/types/local_date.rs index eebbec0b..5c02cc7a 100644 --- a/src/types/local_date.rs +++ b/src/types/local_date.rs @@ -69,11 +69,11 @@ impl LocalDateWrapper { } pub fn get_cql_date(&self) -> CqlDate { - CqlDate(((1 << 31) + self.value) as u32) + CqlDate(((1 << 31) + self.value as i64) as u32) } pub fn from_cql_date(date: CqlDate) -> Self { - let value: i32 = date.0 as i32 - (1 << 31); + let value = (date.0 as i64 - (1 << 31)) as i32; let date = Ymd::from_days(value.into()); LocalDateWrapper { value, From a8eb3367e96375876ccaf6dda7a0c7ae32ca64bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Mon, 10 Mar 2025 14:37:19 +0100 Subject: [PATCH 35/52] Update NAPI-RS to new minor release --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 36e04dc8..53bbd7d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,8 @@ crate-type = ["cdylib"] [dependencies] -napi = { version = "2.12.2", default-features = false, features = ["napi4", "napi6", "async"] } -napi-derive = "2.12.2" +napi = { version = "2.16.16", default-features = false, features = ["napi4", "napi6", "async"] } +napi-derive = "2.16.6" scylla = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "v0.15.0", features = [ "ssl", ] } @@ -21,7 +21,7 @@ thiserror = "2.0.12" [build-dependencies] -napi-build = "2.0.1" +napi-build = "2.1.5" [profile.release] lto = true From 665c8d9aee476b8fee9e883a9d744c48948b8e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Mon, 10 Mar 2025 15:01:38 +0100 Subject: [PATCH 36/52] Update scylla rust driver to 1.0.0 --- Cargo.toml | 4 +- src/auth.rs | 6 ++- src/requests/parameter_wrappers.rs | 5 +- src/requests/request.rs | 2 +- src/result.rs | 87 +++++++++++++++++------------- src/session.rs | 11 ++-- src/tests/request_values_tests.rs | 7 +-- src/tests/result_tests.rs | 6 +-- src/types/duration.rs | 2 +- src/types/local_date.rs | 2 +- src/types/local_time.rs | 2 +- src/types/time_uuid.rs | 2 +- 12 files changed, 71 insertions(+), 65 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 53bbd7d1..47c03a63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,7 @@ crate-type = ["cdylib"] napi = { version = "2.16.16", default-features = false, features = ["napi4", "napi6", "async"] } napi-derive = "2.16.6" -scylla = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "v0.15.0", features = [ - "ssl", -] } +scylla = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "v1.0.0" } tokio = { version = "1.34", features = ["full"] } futures = "0.3" uuid = "1" diff --git a/src/auth.rs b/src/auth.rs index cd9a6c6a..48ab06e0 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,12 +1,14 @@ // This is separate file, but still compiles into the library +use scylla::client::session_builder::SessionBuilder; + #[napi(js_name = "PlainTextAuthProvider")] pub struct PlainTextAuthProvider { // If we use pub, given attribute is accessible in JS: pub id: u32, // Not all attributes can be public: /* pub <- CE */ - internal: scylla::SessionBuilder, + internal: SessionBuilder, /* If a given object can't be translated into js, it can still be in a class exposed to JS, but cannot be public. It's also not visible in the definition in ts file. This **suggests** if we crete an object of this class, @@ -26,7 +28,7 @@ impl PlainTextAuthProvider { println!("Plain text constructor!"); PlainTextAuthProvider { id: 10, - internal: scylla::SessionBuilder::new(), + internal: SessionBuilder::new(), } } diff --git a/src/requests/parameter_wrappers.rs b/src/requests/parameter_wrappers.rs index 861e694c..b9a28741 100644 --- a/src/requests/parameter_wrappers.rs +++ b/src/requests/parameter_wrappers.rs @@ -1,8 +1,5 @@ use napi::bindgen_prelude::{BigInt, Buffer}; -use scylla::frame::{ - response::result::CqlValue, - value::{Counter, CqlTimestamp, CqlTimeuuid}, -}; +use scylla::value::{Counter, CqlTimestamp, CqlTimeuuid, CqlValue}; use crate::{ types::{ diff --git a/src/requests/request.rs b/src/requests/request.rs index 9b80f966..05263f2f 100644 --- a/src/requests/request.rs +++ b/src/requests/request.rs @@ -1,5 +1,5 @@ use napi::bindgen_prelude::BigInt; -use scylla::prepared_statement::PreparedStatement; +use scylla::statement::prepared::PreparedStatement; use crate::{result::map_column_type_to_complex_type, types::type_wrappers::ComplexType}; diff --git a/src/result.rs b/src/result.rs index 3dd03203..115c6f81 100644 --- a/src/result.rs +++ b/src/result.rs @@ -12,9 +12,11 @@ use napi::{ Error, Status, }; use scylla::{ - frame::response::result::{ColumnType, CqlValue, Row}, - transport::query_result::IntoRowsResultError, - QueryResult, QueryRowsResult, + cluster::metadata::{CollectionType, NativeType}, + errors::IntoRowsResultError, + frame::response::result::ColumnType, + response::query_result::{QueryResult, QueryRowsResult}, + value::{CqlValue, Row}, }; use crate::types::duration::DurationWrapper; @@ -159,7 +161,7 @@ impl CqlValueWrapper { #[napi] /// Get type of value in this object pub fn get_type(&self) -> CqlType { - match self.inner { + match &self.inner { CqlValue::Ascii(_) => CqlType::Ascii, CqlValue::BigInt(_) => CqlType::BigInt, CqlValue::Boolean(_) => CqlType::Boolean, @@ -186,6 +188,7 @@ impl CqlValueWrapper { CqlValue::Tuple(_) => CqlType::Tuple, // NOI CqlValue::Uuid(_) => CqlType::Uuid, CqlValue::Varint(_) => CqlType::Varint, // NOI + other => unimplemented!("Missing implementation for CQL value {:?}", other), } } @@ -382,39 +385,51 @@ impl CqlValueWrapper { pub(crate) fn map_column_type_to_complex_type(typ: &ColumnType) -> ComplexType { match typ { - ColumnType::Custom(_) => panic!("No support for custom type"), - ColumnType::Ascii => ComplexType::simple_type(CqlType::Ascii), - ColumnType::Boolean => ComplexType::simple_type(CqlType::Boolean), - ColumnType::Blob => ComplexType::simple_type(CqlType::Blob), - ColumnType::Counter => ComplexType::simple_type(CqlType::Counter), - ColumnType::Date => ComplexType::simple_type(CqlType::Date), - ColumnType::Decimal => ComplexType::simple_type(CqlType::Decimal), - ColumnType::Double => ComplexType::simple_type(CqlType::Double), - ColumnType::Duration => ComplexType::simple_type(CqlType::Duration), - ColumnType::Float => ComplexType::simple_type(CqlType::Float), - ColumnType::Int => ComplexType::simple_type(CqlType::Int), - ColumnType::BigInt => ComplexType::simple_type(CqlType::BigInt), - ColumnType::Text => ComplexType::simple_type(CqlType::Text), - ColumnType::Timestamp => ComplexType::simple_type(CqlType::Timestamp), - ColumnType::Inet => ComplexType::simple_type(CqlType::Inet), - ColumnType::List(t) => { - ComplexType::one_support(CqlType::List, Some(map_column_type_to_complex_type(t))) - } - ColumnType::Map(t, v) => ComplexType::two_support( - CqlType::Map, - Some(map_column_type_to_complex_type(t)), - Some(map_column_type_to_complex_type(v)), - ), - ColumnType::Set(t) => { - ComplexType::one_support(CqlType::Set, Some(map_column_type_to_complex_type(t))) - } + ColumnType::Native(native) => ComplexType::simple_type(match native { + NativeType::Ascii => CqlType::Ascii, + NativeType::Boolean => CqlType::Boolean, + NativeType::Blob => CqlType::Blob, + NativeType::Counter => CqlType::Counter, + NativeType::Date => CqlType::Date, + NativeType::Decimal => CqlType::Decimal, + NativeType::Double => CqlType::Double, + NativeType::Duration => CqlType::Duration, + NativeType::Float => CqlType::Float, + NativeType::Int => CqlType::Int, + NativeType::BigInt => CqlType::BigInt, + NativeType::Text => CqlType::Text, + NativeType::Timestamp => CqlType::Timestamp, + NativeType::Inet => CqlType::Inet, + NativeType::SmallInt => CqlType::SmallInt, + NativeType::TinyInt => CqlType::TinyInt, + NativeType::Time => CqlType::Time, + NativeType::Timeuuid => CqlType::Timeuuid, + NativeType::Uuid => CqlType::Uuid, + NativeType::Varint => CqlType::Varint, + other => unimplemented!("Missing implementation for CQL native type {:?}", other), + }), + ColumnType::Collection { frozen: _, typ } => match typ { + CollectionType::List(column_type) => ComplexType::one_support( + CqlType::List, + Some(map_column_type_to_complex_type(column_type)), + ), + CollectionType::Map(column_type, column_type1) => ComplexType::two_support( + CqlType::Map, + Some(map_column_type_to_complex_type(column_type)), + Some(map_column_type_to_complex_type(column_type1)), + ), + CollectionType::Set(column_type) => ComplexType::one_support( + CqlType::Set, + Some(map_column_type_to_complex_type(column_type)), + ), + other => unimplemented!("Missing implementation for CQL Collection type {:?}", other), + }, ColumnType::UserDefinedType { .. } => ComplexType::simple_type(CqlType::UserDefinedType), - ColumnType::SmallInt => ComplexType::simple_type(CqlType::SmallInt), - ColumnType::TinyInt => ComplexType::simple_type(CqlType::TinyInt), - ColumnType::Time => ComplexType::simple_type(CqlType::Time), - ColumnType::Timeuuid => ComplexType::simple_type(CqlType::Timeuuid), ColumnType::Tuple(_) => ComplexType::simple_type(CqlType::Tuple), - ColumnType::Uuid => ComplexType::simple_type(CqlType::Uuid), - ColumnType::Varint => ComplexType::simple_type(CqlType::Varint), + ColumnType::Vector { + typ: _, + dimensions: _, + } => todo!(), + other => unimplemented!("Missing implementation for CQL type {:?}", other), } } diff --git a/src/session.rs b/src/session.rs index 0beb4690..e2424d33 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,9 +1,10 @@ -use scylla::prepared_statement::PreparedStatement; +use scylla::client::session::Session; +use scylla::client::session_builder::SessionBuilder; +use scylla::client::SelfIdentity; +use scylla::statement::batch::Batch; +use scylla::statement::prepared::PreparedStatement; use scylla::statement::{Consistency, SerialConsistency}; -use scylla::{ - batch::Batch, frame::response::result::CqlValue, transport::SelfIdentity, Session, - SessionBuilder, -}; +use scylla::value::CqlValue; use crate::options; use crate::requests::parameter_wrappers::QueryParameterWrapper; diff --git a/src/tests/request_values_tests.rs b/src/tests/request_values_tests.rs index fa1d446b..3bf959ec 100644 --- a/src/tests/request_values_tests.rs +++ b/src/tests/request_values_tests.rs @@ -1,18 +1,15 @@ +use scylla::value::CqlTime; use std::{ net::{IpAddr, Ipv4Addr}, str::FromStr, }; -use scylla::frame::{ - response::result::CqlValue, - value::{Counter, CqlDuration, CqlTime, CqlTimestamp, CqlTimeuuid}, -}; - use crate::{ requests::parameter_wrappers::QueryParameterWrapper, types::type_wrappers::{ComplexType, CqlType}, }; +use scylla::value::{Counter, CqlDuration, CqlTimestamp, CqlTimeuuid, CqlValue}; use uuid::uuid; #[napi] diff --git a/src/tests/result_tests.rs b/src/tests/result_tests.rs index a60d152b..5b7ae9ff 100644 --- a/src/tests/result_tests.rs +++ b/src/tests/result_tests.rs @@ -1,13 +1,9 @@ +use scylla::value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp, CqlTimeuuid, CqlValue}; use std::{ net::{IpAddr, Ipv4Addr}, str::FromStr, }; -use scylla::frame::{ - response::result::CqlValue, - value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp, CqlTimeuuid}, -}; - use crate::result::CqlValueWrapper; use uuid::uuid; diff --git a/src/types/duration.rs b/src/types/duration.rs index 7a4503bf..19f417f7 100644 --- a/src/types/duration.rs +++ b/src/types/duration.rs @@ -1,5 +1,5 @@ use napi::bindgen_prelude::BigInt; -use scylla::frame::value::CqlDuration; +use scylla::value::CqlDuration; use crate::utils::bigint_to_i64; diff --git a/src/types/local_date.rs b/src/types/local_date.rs index 5c02cc7a..a4e329cd 100644 --- a/src/types/local_date.rs +++ b/src/types/local_date.rs @@ -1,6 +1,6 @@ use crate::utils::{js_error, CharCounter}; use regex::Regex; -use scylla::frame::value::CqlDate; +use scylla::value::CqlDate; use std::sync::LazyLock; use std::{ cmp::max, diff --git a/src/types/local_time.rs b/src/types/local_time.rs index 49084dc4..9aa63ffc 100644 --- a/src/types/local_time.rs +++ b/src/types/local_time.rs @@ -1,6 +1,6 @@ use crate::utils::{bigint_to_i64, js_error, CharCounter}; use napi::bindgen_prelude::BigInt; -use scylla::frame::value::CqlTime; +use scylla::value::CqlTime; use std::fmt::{self, Write}; use std::num::ParseIntError; diff --git a/src/types/time_uuid.rs b/src/types/time_uuid.rs index c2dc6479..d5965d6a 100644 --- a/src/types/time_uuid.rs +++ b/src/types/time_uuid.rs @@ -1,5 +1,5 @@ use napi::bindgen_prelude::Buffer; -use scylla::frame::value::CqlTimeuuid; +use scylla::value::CqlTimeuuid; use super::uuid::UuidWrapper; From 5e28d534ab86feb44e5269ad9a147af0408f66ef Mon Sep 17 00:00:00 2001 From: ZuzaOsa Date: Mon, 10 Mar 2025 22:42:18 +0100 Subject: [PATCH 37/52] Move TS tests out of not-supported directory --- .../typescript/api-generation-test.ts | 4 ++-- test/{unit-not-supported => }/typescript/client-tests.ts | 2 +- test/{unit-not-supported => }/typescript/mapping-tests.ts | 2 +- test/{unit-not-supported => }/typescript/metadata-tests.ts | 2 +- test/{unit-not-supported => }/typescript/policy-tests.ts | 2 +- test/{unit-not-supported => }/typescript/tsconfig.json | 0 test/{unit-not-supported => }/typescript/types-test.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename test/{unit-not-supported => }/typescript/api-generation-test.ts (98%) rename test/{unit-not-supported => }/typescript/client-tests.ts (99%) rename test/{unit-not-supported => }/typescript/mapping-tests.ts (97%) rename test/{unit-not-supported => }/typescript/metadata-tests.ts (95%) rename test/{unit-not-supported => }/typescript/policy-tests.ts (97%) rename test/{unit-not-supported => }/typescript/tsconfig.json (100%) rename test/{unit-not-supported => }/typescript/types-test.ts (97%) diff --git a/test/unit-not-supported/typescript/api-generation-test.ts b/test/typescript/api-generation-test.ts similarity index 98% rename from test/unit-not-supported/typescript/api-generation-test.ts rename to test/typescript/api-generation-test.ts index 83d6beeb..c87c94e1 100644 --- a/test/unit-not-supported/typescript/api-generation-test.ts +++ b/test/typescript/api-generation-test.ts @@ -8,8 +8,8 @@ import { policies, tracker, types, -} from "../../../main"; -import * as root from "../../../main"; +} from "../../main"; +import * as root from "../../main"; let counter: number = 0; diff --git a/test/unit-not-supported/typescript/client-tests.ts b/test/typescript/client-tests.ts similarity index 99% rename from test/unit-not-supported/typescript/client-tests.ts rename to test/typescript/client-tests.ts index e8fe3d36..97aea3e2 100644 --- a/test/unit-not-supported/typescript/client-tests.ts +++ b/test/typescript/client-tests.ts @@ -4,7 +4,7 @@ import { ExecutionProfile, policies, types, -} from "../../../main"; +} from "../../main"; /* * TypeScript definitions compilation tests for Client class. diff --git a/test/unit-not-supported/typescript/mapping-tests.ts b/test/typescript/mapping-tests.ts similarity index 97% rename from test/unit-not-supported/typescript/mapping-tests.ts rename to test/typescript/mapping-tests.ts index 58cdc6b2..a70a3229 100644 --- a/test/unit-not-supported/typescript/mapping-tests.ts +++ b/test/typescript/mapping-tests.ts @@ -1,4 +1,4 @@ -import { Client, mapping, types } from "../../../main"; +import { Client, mapping, types } from "../../main"; import Mapper = mapping.Mapper; import ModelMapper = mapping.ModelMapper; import Uuid = types.Uuid; diff --git a/test/unit-not-supported/typescript/metadata-tests.ts b/test/typescript/metadata-tests.ts similarity index 95% rename from test/unit-not-supported/typescript/metadata-tests.ts rename to test/typescript/metadata-tests.ts index 4277b201..07c9cde0 100644 --- a/test/unit-not-supported/typescript/metadata-tests.ts +++ b/test/typescript/metadata-tests.ts @@ -1,4 +1,4 @@ -import { Client, Host, metadata, types } from "../../../main"; +import { Client, Host, metadata, types } from "../../main"; import TableMetadata = metadata.TableMetadata; import QueryTrace = metadata.QueryTrace; diff --git a/test/unit-not-supported/typescript/policy-tests.ts b/test/typescript/policy-tests.ts similarity index 97% rename from test/unit-not-supported/typescript/policy-tests.ts rename to test/typescript/policy-tests.ts index 66d673e0..c6f326e6 100644 --- a/test/unit-not-supported/typescript/policy-tests.ts +++ b/test/typescript/policy-tests.ts @@ -1,4 +1,4 @@ -import { policies } from "../../../main"; +import { policies } from "../../main"; import LoadBalancingPolicy = policies.loadBalancing.LoadBalancingPolicy; import TokenAwarePolicy = policies.loadBalancing.TokenAwarePolicy; import ReconnectionPolicy = policies.reconnection.ReconnectionPolicy; diff --git a/test/unit-not-supported/typescript/tsconfig.json b/test/typescript/tsconfig.json similarity index 100% rename from test/unit-not-supported/typescript/tsconfig.json rename to test/typescript/tsconfig.json diff --git a/test/unit-not-supported/typescript/types-test.ts b/test/typescript/types-test.ts similarity index 97% rename from test/unit-not-supported/typescript/types-test.ts rename to test/typescript/types-test.ts index df68d6d8..cfe4c94e 100644 --- a/test/unit-not-supported/typescript/types-test.ts +++ b/test/typescript/types-test.ts @@ -1,4 +1,4 @@ -import { types, Client } from "../../../main"; +import { types, Client } from "../../main"; import Uuid = types.Uuid; import TimeUuid = types.TimeUuid; import Long = types.Long; From 71150a55b43ba68985b6000d8b7474e312db335b Mon Sep 17 00:00:00 2001 From: ZuzaOsa Date: Tue, 11 Mar 2025 11:34:07 +0100 Subject: [PATCH 38/52] Add typescript workflow --- .github/workflows/typescript-tests.yml | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/typescript-tests.yml diff --git a/.github/workflows/typescript-tests.yml b/.github/workflows/typescript-tests.yml new file mode 100644 index 00000000..2cf79737 --- /dev/null +++ b/.github/workflows/typescript-tests.yml @@ -0,0 +1,49 @@ +name: Typescript tests +env: + DEBUG: napi:* +"on": + push: + branches: + - "**" +jobs: + build-and-run-tests: + strategy: + fail-fast: false + name: Build and run typescript tests - node@20 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - name: Install + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: x86_64-unknown-linux-gnu + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ~/.napi-rs + .cargo-cache + target/ + key: x86_64-unknown-linux-gnu-cargo-ubuntu-latest + - name: Install dependencies + run: npm i + - name: Run build + run: npm run build + - name: Install TypeScript 4.9 + run: npm install -g typescript@4.9 + - name: Compile TypeScript + run: | + pushd test/typescript/ + tsc -p . + node -e "require('./api-generation-test').generate()" > generated.ts + tsc generated.ts + shell: bash From fa5576c403e96318b1ebc5b9623f4b9b0451a7d4 Mon Sep 17 00:00:00 2001 From: ZuzaOsa Date: Tue, 11 Mar 2025 17:10:16 +0100 Subject: [PATCH 39/52] Remove deprecated modules from Typescript tests Deprecated modules are added to the except list of Typescript tests. The modules are available in JS but not in TS to fail early when used. --- test/typescript/api-generation-test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/typescript/api-generation-test.ts b/test/typescript/api-generation-test.ts index c87c94e1..c6971a6c 100644 --- a/test/typescript/api-generation-test.ts +++ b/test/typescript/api-generation-test.ts @@ -26,8 +26,8 @@ export function generate(): void { console.log(` 'use strict'; -import { auth, concurrent, errors, datastax, mapping, metadata, metrics, policies, tracker, types } from "../../../index"; -import * as root from "../../../index"; +import { auth, concurrent, errors, mapping, metadata, metrics, policies, tracker, types } from "../../main"; +import * as root from "../../main"; export async function generatedFn() { let n:number; @@ -36,10 +36,14 @@ export async function generatedFn() { let f:Function; `); + // We exclude entities that are not meant to be used directly + // and deprecated entites. They are available in JS for compatibility + // (and throw a deprecated message) but are not availabe in Typescript + // to fail at compile time. printClasses(root, "root", new Set(["Encoder"])); - printObjects(root, "root", new Set(["token"])); + printObjects(root, "root", new Set(["token", "datastax", "geometry"])); - printClasses(auth, "auth", new Set(["NoAuthProvider"])); + printClasses(auth, "auth", new Set(["NoAuthProvider", "DseGssapiAuthProvider", "DsePlainTextAuthProvider"])); printClasses(errors, "errors"); printFunctions(concurrent, "concurrent"); printClasses(concurrent, "concurrent"); From abc5dbaee334c0e195630f31ca1c352e3bfb0fd4 Mon Sep 17 00:00:00 2001 From: ZuzaOsa Date: Tue, 11 Mar 2025 16:45:20 +0100 Subject: [PATCH 40/52] Change variable name from case to case_id Typescript does not allow case as a variable name. --- src/tests/utils_tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/utils_tests.rs b/src/tests/utils_tests.rs index b031f2a2..2f094140 100644 --- a/src/tests/utils_tests.rs +++ b/src/tests/utils_tests.rs @@ -3,15 +3,15 @@ use napi::bindgen_prelude::BigInt; use crate::utils::{self, bigint_to_i64}; #[napi] -pub fn tests_bigint_to_i64(value: BigInt, case: Option) -> napi::Result<()> { - let case = match case { - Some(case) => case, +pub fn tests_bigint_to_i64(value: BigInt, case_id: Option) -> napi::Result<()> { + let case_id = match case_id { + Some(case_id) => case_id, None => { return utils::bigint_to_i64(value, "Overflow expected").map(|_| ()); } }; - let expected = match case { + let expected = match case_id { 0 => 0, 1 => -1, 2 => 5, From d0c890bf3232342652b450573a59bfb41b7d573e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Fri, 14 Mar 2025 17:01:35 +0100 Subject: [PATCH 41/52] Fix circular dependency warning --- lib/types/type-guessing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types/type-guessing.js b/lib/types/type-guessing.js index 6e84c433..8b175ce1 100644 --- a/lib/types/type-guessing.js +++ b/lib/types/type-guessing.js @@ -1,4 +1,4 @@ -const { errors } = require("../../main"); +const { errors } = require("../errors"); const types = require("./index"); const typeValues = types.dataTypes; const Long = types.Long; From 7a1a8c87e6fc62f78af09a997f0e312121aeb555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Fri, 14 Mar 2025 16:50:38 +0100 Subject: [PATCH 42/52] Add pedantic mode In pedantic mode any nodeJS warning will be treated as an error --- .github/workflows/examples.yml | 1 + .github/workflows/integration-tests.yml | 1 + .github/workflows/unit-tests.yml | 1 + main.js | 11 +++++++++++ 4 files changed, 14 insertions(+) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index d83871df..d0e2b115 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -3,6 +3,7 @@ name: Run examples env: DEBUG: napi:* APP_NAME: scylladb-javascript-driver + PEDANTIC: true "on": push: branches: diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 91b193ff..4f8bef91 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -5,6 +5,7 @@ env: DEBUG: napi:* APP_NAME: scylladb-javascript-driver RUST_BACKTRACE: full + PEDANTIC: true "on": push: branches: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 30b3e065..c3255107 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -3,6 +3,7 @@ name: Unit tests env: DEBUG: napi:* + PEDANTIC: true "on": push: branches: diff --git a/main.js b/main.js index dd840f93..a8dcb9a2 100644 --- a/main.js +++ b/main.js @@ -1,5 +1,16 @@ "use strict"; +// In pedantic mode, when any warnings are detected, exit program imminently +const process = require('node:process'); +if (process.env.PEDANTIC == "true") { + process.on('warning', (warning) => { + console.warn(`Warning found in pedantic mode:`); + console.warn(warning.message); + console.warn(warning.stack); + process.exit(1); + }); +} + const clientOptions = require("./lib/client-options"); exports.Client = require("./lib/client"); exports.ExecutionProfile = require("./lib/execution-profile").ExecutionProfile; From 35c0bfe35bb3eadb0949dd26dd0f27a14328de87 Mon Sep 17 00:00:00 2001 From: Piotr Maksymiuk Date: Tue, 18 Mar 2025 12:31:10 +0100 Subject: [PATCH 43/52] Change names of Github Actions from sentences equivalent to sentences --- .github/workflows/code-quality.yml | 2 +- .github/workflows/typescript-tests.yml | 2 +- .github/workflows/unit-tests.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 87597a20..e0e4bd7c 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -1,4 +1,4 @@ -name: Code quality +name: Check code quality env: DEBUG: napi:* APP_NAME: scylladb-javascript-driver diff --git a/.github/workflows/typescript-tests.yml b/.github/workflows/typescript-tests.yml index 2cf79737..0473fcc4 100644 --- a/.github/workflows/typescript-tests.yml +++ b/.github/workflows/typescript-tests.yml @@ -1,4 +1,4 @@ -name: Typescript tests +name: Run Typescript tests env: DEBUG: napi:* "on": diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c3255107..50366e00 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,6 +1,6 @@ # The goal of this workflow is to check if unit test run correctly # This includes both datastax unit tests and tests specific for this driver -name: Unit tests +name: Run unit tests env: DEBUG: napi:* PEDANTIC: true From 76bb98da1434ee18dbe3106a3e0c25f73b7662ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Sat, 8 Mar 2025 11:18:02 +0100 Subject: [PATCH 44/52] Fix integration tests problems Integration tests appear to have issues in certain conditions, when run on github CI, which depends on the order in which they are run, which tests are enabled (there is no single failing test). I was unable to reproduce the issue on any other machine than Github workers. The issue appears as one of the following problems: - capacity overflow - memory allocation failure After some investigation, while I wasn't able to pinpoint a specific issue, I determined that the issue arises from concurrent JS code interacting with the NAPI-RS layer. This commit attempts to stop the problems, by removing the concurrency. This feature is controlled by NO_CONCURRENCY env variable. The concurrency is removed, only when this variable is set. This severely limits the speed at which executeConcurrent is performed, but appears to fully resolve the issue. With this in mind, any concurrency should be implemented on Rust side, or the root issue should be determined before removing the concurrency restrictions. --- lib/concurrent/index.js | 12 ++++++++++-- package-lock.json | 16 ++++++++++++++++ package.json | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/concurrent/index.js b/lib/concurrent/index.js index 58d1c3d7..e5c7c56f 100644 --- a/lib/concurrent/index.js +++ b/lib/concurrent/index.js @@ -2,6 +2,8 @@ const { Stream } = require("stream"); const utils = require("../utils"); +const { Mutex } = require("async-mutex"); +const { env } = require("process"); /** * Utilities for concurrent query execution with the DataStax Node.js Driver. @@ -109,6 +111,9 @@ class ArrayBasedExecutor { options.concurrencyLevel || 100, this._parameters.length, ); + if (env.NO_CONCURRENCY) { + this._concurrencyLevel = 1; + } this._queryOptions = { prepare: true, executionProfile: options.executionProfile, @@ -176,6 +181,7 @@ class StreamBasedExecutor { * @private */ constructor(client, query, stream, options) { + this._mutex = new Mutex(); this._client = client; this._query = query; this._stream = stream; @@ -206,7 +212,7 @@ class StreamBasedExecutor { }); } - _executeOne(params) { + async _executeOne(params) { if (!Array.isArray(params)) { return this._setReadEnded( new TypeError( @@ -221,8 +227,9 @@ class StreamBasedExecutor { return; } - const index = this._index++; this._inFlight++; + const release = env.NO_CONCURRENCY ? await this._mutex.acquire() : () => { }; + const index = this._index++; this._client .execute(this._query, params, this._queryOptions) @@ -235,6 +242,7 @@ class StreamBasedExecutor { this._setError(index, err); }) .then(() => { + release(); if (this._stream.isPaused()) { this._stream.resume(); } diff --git a/package-lock.json b/package-lock.json index ef4178c1..2ea41f6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/long": "~5.0.0", "@types/node": ">=8", "adm-zip": "~0.5.10", + "async-mutex": "^0.5.0", "jsdoc": "^4.0.4", "long": "~5.2.3" }, @@ -677,6 +678,15 @@ "node": "*" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/async-sema": { "version": "3.1.1", "dev": true, @@ -3896,6 +3906,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "dev": true, diff --git a/package.json b/package.json index cc5f9da0..fb7a8a30 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/long": "~5.0.0", "@types/node": ">=8", "adm-zip": "~0.5.10", + "async-mutex": "^0.5.0", "jsdoc": "^4.0.4", "long": "~5.2.3" }, From 339d96cb046831bb5438f5851b5b45b349596e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Thu, 20 Mar 2025 09:46:24 +0100 Subject: [PATCH 45/52] Block concurrency on github CI integration tests --- .github/workflows/integration-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 4f8bef91..c0514f02 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -6,6 +6,7 @@ env: APP_NAME: scylladb-javascript-driver RUST_BACKTRACE: full PEDANTIC: true + NO_CONCURRENCY: true "on": push: branches: From 9d84fce10d40a58aa27374ab160cacc3884022f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 11 Mar 2025 10:27:22 +0100 Subject: [PATCH 46/52] Check values of the map Ensure map keys and elements are not null. --- lib/types/cql-utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index dff94525..66f386e9 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -80,6 +80,8 @@ function encodeMap(value, parentType) { continue; } const val = value[key]; + ensureValue(val, "A collection can't contain null or unset values"); + ensureValue(key, "A collection can't contain null or unset values"); res.push([ getWrapped(keySubtype || guessTypeChecked(key), key), getWrapped(valueSubtype || guessTypeChecked(val), val), From 4cb224ca5d2eb18cc762cb36b29e786fc76d7385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 4 Mar 2025 09:21:45 +0100 Subject: [PATCH 47/52] Convert selected types from string To have parity with the old driver, following types can be provided as a string in expected format instead of providing object of given class type. Timestamp Intet UUID TimeUUID LocalDate LocalTime --- lib/types/cql-utils.js | 46 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index 66f386e9..f2336cf4 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -101,6 +101,7 @@ function getWrapped(type, value) { return null; } let encodedElement; + let tmpElement; // To increase clarity of the error messages, in case value is of different type then expected, // when we call some methods on value variable, type is checked explicitly. // In other cases default Error will be thrown, which has message meaningful for the user. @@ -154,12 +155,32 @@ function getWrapped(type, value) { case rust.CqlType.Text: return rust.QueryParameterWrapper.fromText(value); case rust.CqlType.Timestamp: + tmpElement = value; + if (typeof value === "string") { + value = new Date(value); + } + if (value instanceof Date) { + // milliseconds since epoch + value = value.getTime(); + if (isNaN(value)) { + throw new TypeError("Invalid date: " + tmpElement); + } + } + return rust.QueryParameterWrapper.fromTimestamp(BigInt(value)); case rust.CqlType.Inet: - if (!(value instanceof InetAddress)) + // Other forms of providing InetAddress are kept as parity with old driver + if (typeof value === "string") { + value = InetAddress.fromString(value); + } else if (value instanceof Buffer) { + value = new InetAddress(value); + } + + if (!(value instanceof InetAddress)) { throw new TypeError( "Expected InetAddress type to parse into Cql Inet", ); + } return rust.QueryParameterWrapper.fromInet(value.getInternal()); case rust.CqlType.List: encodedElement = encodeListLike(value, type.getFirstSupportType()); @@ -180,26 +201,43 @@ function getWrapped(type, value) { case rust.CqlType.SmallInt: return rust.QueryParameterWrapper.fromSmallInt(value); case rust.CqlType.Time: - if (!(value instanceof LocalTime)) + // Other forms of providing LocalTime are kept as parity with old driver + if (typeof value == "string") { + value = LocalTime.fromString(value); + } + if (!(value instanceof LocalTime)) { throw new TypeError( "Expected LocalTime type to parse into Cql Time", ); + } + return rust.QueryParameterWrapper.fromLocalTime( value.getInternal(), ); case rust.CqlType.TinyInt: return rust.QueryParameterWrapper.fromTinyInt(value); case rust.CqlType.Uuid: - if (!(value instanceof Uuid)) + // Other forms of providing UUID are kept as parity with old driver + if (typeof value === "string") { + value = Uuid.fromString(value); + } + + if (!(value instanceof Uuid)) { throw new TypeError( "Expected UUID type to parse into Cql Uuid", ); + } return rust.QueryParameterWrapper.fromUuid(value.getInternal()); case rust.CqlType.Timeuuid: - if (!(value instanceof TimeUuid)) + // Other forms of providing TimeUUID are kept as parity with old driver + if (typeof value === "string") { + value = TimeUuid.fromString(value); + } + if (!(value instanceof TimeUuid)) { throw new TypeError( "Expected Time UUID type to parse into Cql Uuid", ); + } return rust.QueryParameterWrapper.fromTimeUuid(value.getInternal()); default: // Or not yet implemented type From 970392c9287451de7e434b63bb20f384f511a695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 4 Mar 2025 09:22:01 +0100 Subject: [PATCH 48/52] Number validation --- lib/types/cql-utils.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index f2336cf4..22e5e238 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -90,6 +90,16 @@ function encodeMap(value, parentType) { return res; } +/** + * Ensures the provided value is non NaN. If it is, throws an error + * @param {any} value + */ +function ensureNumber(value) { + if (Number.isNaN(value)) { + throw new TypeError("Expected Number, obtained " + util.inspect(value)); + } +} + /** * Wrap value into QueryParameterWrapper based on the type * @param {rust.ComplexType} type @@ -135,6 +145,7 @@ function getWrapped(type, value) { case rust.CqlType.Float: return rust.QueryParameterWrapper.fromFloat(value); case rust.CqlType.Int: + ensureNumber(value); return rust.QueryParameterWrapper.fromInt(value); case rust.CqlType.Set: // TODO: @@ -199,6 +210,7 @@ function getWrapped(type, value) { encodedElement = encodeMap(value, type); return rust.QueryParameterWrapper.fromMap(encodedElement); case rust.CqlType.SmallInt: + ensureNumber(value); return rust.QueryParameterWrapper.fromSmallInt(value); case rust.CqlType.Time: // Other forms of providing LocalTime are kept as parity with old driver @@ -215,6 +227,7 @@ function getWrapped(type, value) { value.getInternal(), ); case rust.CqlType.TinyInt: + ensureNumber(value); return rust.QueryParameterWrapper.fromTinyInt(value); case rust.CqlType.Uuid: // Other forms of providing UUID are kept as parity with old driver From f6c2809efc6cf17b0d5fc24c47269be823c3b5ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 4 Mar 2025 09:30:08 +0100 Subject: [PATCH 49/52] BigInt type extension --- lib/new-utils.js | 17 +++++++++++++++++ lib/types/cql-utils.js | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/new-utils.js b/lib/new-utils.js index 00f45b97..935b3e09 100644 --- a/lib/new-utils.js +++ b/lib/new-utils.js @@ -1,5 +1,6 @@ "use strict"; const customErrors = require("./errors"); +const util = require("./utils"); const Long = require("long"); @@ -88,8 +89,24 @@ function longToBigint(from) { return r; } +/** + * Ensure the value is one of the accepted numeric types, and convert them to BigInt + * @param {string | number | Long | BigInt} value + */ +function arbitraryValueToBigInt(value) { + if (typeof value === "bigint") return value; + if (typeof value === "string" || typeof value == "number") + return BigInt(value); + if (value instanceof Long) return longToBigint(value); + + throw new TypeError( + "Not a valid BigInt value, obtained " + util.inspect(value), + ); +} + exports.throwNotSupported = throwNotSupported; exports.napiErrorHandler = napiErrorHandler; exports.throwNotSupported = throwNotSupported; exports.bigintToLong = bigintToLong; exports.longToBigint = longToBigint; +exports.arbitraryValueToBigInt = arbitraryValueToBigInt; diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index 22e5e238..efb29828 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -11,6 +11,7 @@ const LocalDate = require("./local-date"); const LocalTime = require("./local-time"); const InetAddress = require("./inet-address"); const { guessType } = require("./type-guessing"); +const { arbitraryValueToBigInt } = require("../new-utils"); /** * Ensures the value isn't one of many ways to express null value @@ -119,7 +120,9 @@ function getWrapped(type, value) { case rust.CqlType.Ascii: return rust.QueryParameterWrapper.fromAscii(value); case rust.CqlType.BigInt: - return rust.QueryParameterWrapper.fromBigint(value); + return rust.QueryParameterWrapper.fromBigint( + arbitraryValueToBigInt(value), + ); case rust.CqlType.Blob: return rust.QueryParameterWrapper.fromBlob(value); case rust.CqlType.Boolean: From aa0d16c92e06b72012dafe5b5005af29959f4008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Mon, 10 Mar 2025 09:20:38 +0100 Subject: [PATCH 50/52] Remove no longer supported encodings from tests As we decided, we no longer want to accept numbers as strings. This commits removes (currently disabled) tests for those encodings. --- test/integration/supported/numeric-tests.js | 26 +++------------------ 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/test/integration/supported/numeric-tests.js b/test/integration/supported/numeric-tests.js index cdffd47a..b3af14ef 100644 --- a/test/integration/supported/numeric-tests.js +++ b/test/integration/supported/numeric-tests.js @@ -96,31 +96,15 @@ module.exports = function (keyspace, prepare) { it("should support setting numeric values using strings for collections", () => { const insertQuery = `INSERT INTO tbl_numeric_collections - (id, list_bigint, list_decimal, map_double, set_float, map_varint, set_int) VALUES (?, ?, ?, ?, ?, ?, ?)`; + (id, list_bigint, list_decimal, map_varint) VALUES (?, ?, ?, ?)`; const hints = !prepare - ? [ - null, - "list", - "list", - "map", - "set", - "map", - "set", - ] + ? [null, "list", "list", "map"] : null; const intValue = "890"; const decimalValue = "1234567.875"; const id = Uuid.random(); - const params = [ - id, - [intValue], - [decimalValue], - { a: decimalValue }, - [decimalValue], - { a: intValue }, - [intValue], - ]; + const params = [id, [intValue], [decimalValue], { a: intValue }]; return client .execute(insertQuery, params, { prepare, hints }) @@ -138,10 +122,6 @@ module.exports = function (keyspace, prepare) { expect(row["list_decimal"][0].toString()).to.be.equal( decimalValue, ); - expect(row["set_float"][0].toString()).to.be.equal( - decimalValue, - ); - expect(row["set_int"][0].toString()).to.be.equal(intValue); }); }); From 9715c773c2fd2619b5cc0ef2905ff03a22b4c85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Thu, 20 Feb 2025 08:55:12 +0100 Subject: [PATCH 51/52] Add support for advanced decoding CQL BigInt Allows to decode CQL BigInt into either BigInt type or Long type, according to provided configuration option. This still requires the configuration option to be correctly passed from the option provided by the client into the rust part of the code. --- lib/types/result-set.js | 7 ++++++- lib/types/results-wrapper.js | 11 ++++++++--- src/result.rs | 22 +++++++++++++++++++++- test/unit/cql-value-wrapper-tests.js | 10 ++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lib/types/result-set.js b/lib/types/result-set.js index fa3e22cd..21a2f267 100644 --- a/lib/types/result-set.js +++ b/lib/types/result-set.js @@ -81,6 +81,8 @@ class ResultSet { } return; } + + let decodingOptions = result.getDecodingOptions(); /** * Gets an array rows returned by the query. * When the result set represents a response from a write query, this property will be undefined. @@ -89,7 +91,10 @@ class ResultSet { * following pages of results. * @type {Array|undefined} */ - this.rows = resultsWrapper.getRowsFromResultsWrapper(result); + this.rows = resultsWrapper.getRowsFromResultsWrapper( + result, + decodingOptions, + ); /** * Gets the row length of the result, regardless if the result has been buffered or not diff --git a/lib/types/results-wrapper.js b/lib/types/results-wrapper.js index 0cbed90e..1dc3648c 100644 --- a/lib/types/results-wrapper.js +++ b/lib/types/results-wrapper.js @@ -13,9 +13,10 @@ const Row = require("./row"); /** * Checks the type of value wrapper object and gets it from the underlying value * @param {rust.CqlValueWrapper} field + * @param {rust.DecodingOptions | null} decodingOptions * @returns {any} */ -function getCqlObject(field) { +function getCqlObject(field, decodingOptions) { if (field == null) return null; let type = field.getType(); let res, fields; @@ -23,6 +24,9 @@ function getCqlObject(field) { case rust.CqlType.Ascii: return field.getAscii(); case rust.CqlType.BigInt: + if (decodingOptions && decodingOptions.useBigIntAsLong) { + return field.getBigint(); + } return bigintToLong(field.getBigint()); case rust.CqlType.Blob: return field.getBlob(); @@ -87,9 +91,10 @@ function getCqlObject(field) { * Simple way of getting results from rust driver. * Call the driver O(columns * rows) times * @param {rust.QueryResultWrapper} result + * @param {rust.DecodingOptions} decodingOptions * @returns {Array | undefined} Returns array of rows if ResultWrapper has any, and undefined otherwise */ -function getRowsFromResultsWrapper(result) { +function getRowsFromResultsWrapper(result, decodingOptions) { let rustRows = result.getRows(); if (rustRows == null) { // Empty results are treated as undefined @@ -105,7 +110,7 @@ function getRowsFromResultsWrapper(result) { for (let j = 0; j < cols.length; j++) { // By default driver returns row as a map: // column name -> column value - collectedRow[colNames[j]] = getCqlObject(cols[j]); + collectedRow[colNames[j]] = getCqlObject(cols[j], decodingOptions); } rows.push(collectedRow); } diff --git a/src/result.rs b/src/result.rs index 115c6f81..f146c95b 100644 --- a/src/result.rs +++ b/src/result.rs @@ -29,9 +29,17 @@ enum QueryResultVariant { RowsResult(QueryRowsResult), } +#[napi] +#[derive(Clone)] +pub struct EncodingOptions { + pub use_big_int_as_long: bool, + pub use_big_int_as_varint: bool, +} + #[napi] pub struct QueryResultWrapper { internal: QueryResultVariant, + decoding_options: EncodingOptions, } #[napi] @@ -62,7 +70,14 @@ impl QueryResultWrapper { return Err(err_to_napi(e)); } }; - Ok(QueryResultWrapper { internal: value }) + let empty_decoding_options = EncodingOptions { + use_big_int_as_long: false, + use_big_int_as_varint: false, + }; + Ok(QueryResultWrapper { + internal: value, + decoding_options: empty_decoding_options, + }) } #[napi] @@ -134,6 +149,11 @@ impl QueryResultWrapper { QueryResultVariant::EmptyResult(v) => v.tracing_id().map(UuidWrapper::from_cql_uuid), } } + + #[napi] + pub fn get_decoding_options(&self) -> EncodingOptions { + self.decoding_options.clone() + } } #[napi] diff --git a/test/unit/cql-value-wrapper-tests.js b/test/unit/cql-value-wrapper-tests.js index 5efd00c9..3b895538 100644 --- a/test/unit/cql-value-wrapper-tests.js +++ b/test/unit/cql-value-wrapper-tests.js @@ -34,6 +34,16 @@ describe("Cql value wrapper", function () { assert.strictEqual(Long.fromString("69").equals(value), true); }); + it("should get bigInt type correctly from napi, as bigint", function () { + let element = rust.testsGetCqlWrapperBigint(); + let type = element.getType(); + assert.strictEqual(type, rust.CqlType.BigInt); + let value = getCqlObject(element, { useBigIntAsLong: true }); + /* Corresponding value: + let element = CqlValue::BigInt(69); */ + assert.strictEqual(BigInt(69), value); + }); + it("should get boolean type correctly from napi", function () { let element = rust.testsGetCqlWrapperBoolean(); let type = element.getType(); From de3fb559639c1f57f4fbd9ea4a9d9fd99d5c369a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Mon, 24 Mar 2025 15:31:53 +0100 Subject: [PATCH 52/52] Add encoding options to session options --- src/result.rs | 27 ++++++++++++++++++++------- src/session.rs | 16 +++++++++++++--- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/result.rs b/src/result.rs index f146c95b..3069a365 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,4 +1,5 @@ use crate::{ + session::SessionWrapper, types::{ local_date::LocalDateWrapper, time_uuid::TimeUuidWrapper, @@ -29,7 +30,7 @@ enum QueryResultVariant { RowsResult(QueryRowsResult), } -#[napi] +#[napi(object)] #[derive(Clone)] pub struct EncodingOptions { pub use_big_int_as_long: bool, @@ -62,7 +63,10 @@ pub struct MetaColumnWrapper { #[napi] impl QueryResultWrapper { - pub fn from_query(internal: QueryResult) -> napi::Result { + pub fn from_query( + internal: QueryResult, + client: &SessionWrapper, + ) -> napi::Result { let value = match internal.into_rows_result() { Ok(v) => QueryResultVariant::RowsResult(v), Err(IntoRowsResultError::ResultNotRows(v)) => QueryResultVariant::EmptyResult(v), @@ -70,13 +74,13 @@ impl QueryResultWrapper { return Err(err_to_napi(e)); } }; - let empty_decoding_options = EncodingOptions { - use_big_int_as_long: false, - use_big_int_as_varint: false, - }; Ok(QueryResultWrapper { internal: value, - decoding_options: empty_decoding_options, + decoding_options: client + .options + .encoding_options + .clone() + .unwrap_or(EncodingOptions::default()), }) } @@ -453,3 +457,12 @@ pub(crate) fn map_column_type_to_complex_type(typ: &ColumnType) -> ComplexType { other => unimplemented!("Missing implementation for CQL type {:?}", other), } } + +impl EncodingOptions { + pub(crate) fn default() -> EncodingOptions { + EncodingOptions { + use_big_int_as_long: true, + use_big_int_as_varint: false, + } + } +} diff --git a/src/session.rs b/src/session.rs index e2424d33..5e883f25 100644 --- a/src/session.rs +++ b/src/session.rs @@ -9,16 +9,19 @@ use scylla::value::CqlValue; use crate::options; use crate::requests::parameter_wrappers::QueryParameterWrapper; use crate::requests::request::QueryOptionsWrapper; +use crate::result::EncodingOptions; use crate::utils::{bigint_to_i64, js_error}; use crate::{ requests::request::PreparedStatementWrapper, result::QueryResultWrapper, utils::err_to_napi, }; #[napi] +#[derive(Clone)] pub struct SessionOptions { pub connect_points: Vec, pub application_name: Option, pub application_version: Option, + pub encoding_options: Option, } #[napi] @@ -29,6 +32,7 @@ pub struct BatchWrapper { #[napi] pub struct SessionWrapper { inner: Session, + pub(crate) options: SessionOptions, } #[napi] @@ -39,6 +43,7 @@ impl SessionOptions { connect_points: vec![], application_name: None, application_version: None, + encoding_options: None, } } } @@ -53,7 +58,10 @@ impl SessionWrapper { .build() .await .map_err(err_to_napi)?; - Ok(SessionWrapper { inner: s }) + Ok(SessionWrapper { + inner: s, + options: options.clone(), + }) } #[napi] @@ -72,7 +80,7 @@ impl SessionWrapper { .query_unpaged(query, &[]) .await .map_err(err_to_napi)?; - QueryResultWrapper::from_query(query_result) + QueryResultWrapper::from_query(query_result, self) } /// Executes unprepared query. This assumes the types will be either guessed or provided by user. @@ -93,7 +101,7 @@ impl SessionWrapper { .query_unpaged(query, params_vec) .await .map_err(err_to_napi)?; - QueryResultWrapper::from_query(query_result) + QueryResultWrapper::from_query(query_result, &self) } /// Prepares a statement through rust driver for a given session @@ -131,6 +139,7 @@ impl SessionWrapper { .execute_unpaged(&query, params_vec) .await .map_err(err_to_napi)?, + self, ) } @@ -149,6 +158,7 @@ impl SessionWrapper { .batch(&batch.inner, params_vec) .await .map_err(err_to_napi)?, + self, ) } }