From 4a01780ef6ae39b267c9694c76cd6a24c76fa329 Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Tue, 27 Aug 2024 15:12:20 -0300 Subject: [PATCH 01/13] docs(examples): refactor test checkpoint will be removed later Signed-off-by: Daniel Boll --- examples/refactor-test.mts | 19 +++++ index.d.ts | 6 +- src/helpers/query_results.rs | 127 +++++++++++++++++++--------------- src/session/scylla_session.rs | 12 ++-- 4 files changed, 99 insertions(+), 65 deletions(-) create mode 100644 examples/refactor-test.mts diff --git a/examples/refactor-test.mts b/examples/refactor-test.mts new file mode 100644 index 0000000..71b7ccb --- /dev/null +++ b/examples/refactor-test.mts @@ -0,0 +1,19 @@ +import { Cluster } from "../index.js"; + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; + +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ nodes }); +const session = await cluster.connect(); + +await session.execute( + "CREATE KEYSPACE IF NOT EXISTS refactor WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", +); +await session.useKeyspace("refactor"); + +await session.execute( + "CREATE TABLE IF NOT EXISTS refactor (a text, b int, c uuid, d bigint, primary key (a))", +); + +await session.execute("INSERT INTO basic (a, b, c) VALUES (1, 2, 'abc')"); diff --git a/index.d.ts b/index.d.ts index 4f8002d..c675e97 100644 --- a/index.d.ts +++ b/index.d.ts @@ -173,8 +173,8 @@ export class ScyllaSession { * driver does not check it by itself, so incorrect data will be written if the order is * wrong. */ - execute(query: string | Query | PreparedStatement, parameters?: Array> | undefined | null, options?: QueryOptions | undefined | null): Promise - query(scyllaQuery: Query, parameters?: Array> | undefined | null): Promise + execute(query: string | Query | PreparedStatement, parameters?: Array> | undefined | null, options?: QueryOptions | undefined | null): Promise>> + query(scyllaQuery: Query, parameters?: Array> | undefined | null): Promise>> prepare(query: string): Promise /** * Perform a batch query\ @@ -213,7 +213,7 @@ export class ScyllaSession { * console.log(await session.execute("SELECT * FROM users")); * ``` */ - batch(batch: BatchStatement, parameters: Array> | undefined | null>): Promise + batch(batch: BatchStatement, parameters: Array> | undefined | null>): Promise>> /** * Sends `USE ` request on all connections\ * This allows to write `SELECT * FROM table` instead of `SELECT * FROM keyspace.table`\ diff --git a/src/helpers/query_results.rs b/src/helpers/query_results.rs index a9216c9..0c336c3 100644 --- a/src/helpers/query_results.rs +++ b/src/helpers/query_results.rs @@ -1,96 +1,111 @@ +use std::collections::HashMap; + +use napi::bindgen_prelude::{BigInt, Either4}; use scylla::frame::response::result::{ColumnType, CqlValue}; + +use crate::types::uuid::Uuid; pub struct QueryResult { pub(crate) result: scylla::QueryResult, } impl QueryResult { - pub fn parser(result: scylla::QueryResult) -> serde_json::Value { + pub fn parser( + result: scylla::QueryResult, + ) -> Vec>> { if result.result_not_rows().is_ok() || result.rows.is_none() { - return serde_json::json!([]); + return Default::default(); } let rows = result.rows.unwrap(); let column_specs = result.col_specs; - let mut result_json = serde_json::json!([]); + let mut result_json: Vec>> = vec![]; for row in rows { - let mut row_object = serde_json::Map::new(); + let mut row_object: HashMap> = HashMap::new(); for (i, column) in row.columns.iter().enumerate() { let column_name = column_specs[i].name.clone(); let column_value = Self::parse_value(column, &column_specs[i].typ); - row_object.insert(column_name, column_value); + if let Some(column_value) = column_value { + row_object.insert(column_name, column_value); + } } - result_json - .as_array_mut() - .unwrap() - .push(serde_json::Value::Object(row_object)); + result_json.push(row_object); } result_json } - fn parse_value(column: &Option, column_type: &ColumnType) -> serde_json::Value { + fn parse_value( + column: &Option, + column_type: &ColumnType, + ) -> Option> { match column { - Some(column) => match column_type { - ColumnType::Ascii => serde_json::Value::String(column.as_ascii().unwrap().to_string()), - ColumnType::Text => serde_json::Value::String(column.as_text().unwrap().to_string()), - ColumnType::Uuid => serde_json::Value::String(column.as_uuid().unwrap().to_string()), - ColumnType::Int => serde_json::Value::Number( - serde_json::Number::from_f64(column.as_int().unwrap() as f64).unwrap(), - ), - ColumnType::Float => serde_json::Value::Number( - serde_json::Number::from_f64(column.as_float().unwrap() as f64).unwrap(), - ), - ColumnType::Timestamp | ColumnType::Date => { - serde_json::Value::String(column.as_cql_date().unwrap().0.to_string()) - } - ColumnType::UserDefinedType { field_types, .. } => { - Self::parse_udt(column.as_udt().unwrap(), field_types) - } - ColumnType::Boolean => serde_json::Value::Bool(column.as_boolean().unwrap()), - ColumnType::Inet => serde_json::Value::String(column.as_inet().unwrap().to_string()), - ColumnType::Double => serde_json::Value::Number( - serde_json::Number::from_f64(column.as_double().unwrap()).unwrap(), - ), - ColumnType::SmallInt => serde_json::Value::Number( - serde_json::Number::from_f64(column.as_smallint().unwrap() as f64).unwrap(), - ), - ColumnType::TinyInt => serde_json::Value::Number( - serde_json::Number::from_f64(column.as_tinyint().unwrap() as f64).unwrap(), - ), - ColumnType::BigInt => "ColumnType BigInt not supported yet".into(), - ColumnType::Decimal => "ColumnType Decimal not supported yet".into(), - ColumnType::Duration => "ColumnType Duration not supported yet".into(), - ColumnType::Custom(_) => "ColumnType Custom not supported yet".into(), - ColumnType::Blob => "ColumnType Blob not supported yet".into(), - ColumnType::Counter => "ColumnType Counter not supported yet".into(), - ColumnType::List(_) => "ColumnType List not supported yet".into(), - ColumnType::Map(_, _) => "ColumnType Map not supported yet".into(), - ColumnType::Set(_) => "ColumnType Set not supported yet".into(), - ColumnType::Time => "ColumnType Time not supported yet".into(), - ColumnType::Timeuuid => "ColumnType Timeuuid not supported yet".into(), - ColumnType::Tuple(_) => "ColumnType Tuple not supported yet".into(), - ColumnType::Varint => "ColumnType Varint not supported yet".into(), - }, - None => serde_json::Value::Null, + Some(column) => Some(match column_type { + ColumnType::Ascii => Either4::A(column.as_ascii().unwrap().to_string()), + ColumnType::Text => Either4::A(column.as_text().unwrap().to_string()), + ColumnType::Uuid => Either4::D(Uuid { + uuid: column.as_uuid().unwrap(), + }), + ColumnType::BigInt => Either4::C(column.as_bigint().unwrap().into()), + ColumnType::Int => Either4::B(column.as_int().unwrap()), + // ColumnType::Int => serde_json::Value::Number( + // serde_json::Number::from_f64(column.as_int().unwrap() as f64).unwrap(), + // ), + // ColumnType::Float => serde_json::Value::Number( + // serde_json::Number::from_f64(column.as_float().unwrap() as f64).unwrap(), + // ), + // ColumnType::Timestamp | ColumnType::Date => { + // serde_json::Value::String(column.as_cql_date().unwrap().0.to_string()) + // } + // ColumnType::UserDefinedType { field_types, .. } => { + // Self::parse_udt(column.as_udt().unwrap(), field_types) + // } + // ColumnType::Boolean => serde_json::Value::Bool(column.as_boolean().unwrap()), + // ColumnType::Inet => serde_json::Value::String(column.as_inet().unwrap().to_string()), + // ColumnType::Double => serde_json::Value::Number( + // serde_json::Number::from_f64(column.as_double().unwrap()).unwrap(), + // ), + // ColumnType::SmallInt => serde_json::Value::Number( + // serde_json::Number::from_f64(column.as_smallint().unwrap() as f64).unwrap(), + // ), + // ColumnType::TinyInt => serde_json::Value::Number( + // serde_json::Number::from_f64(column.as_tinyint().unwrap() as f64).unwrap(), + // ), + ColumnType::Decimal => Either4::A("ColumnType Decimal not supported yet".to_string()), + ColumnType::Duration => Either4::A("ColumnType Duration not supported yet".to_string()), + ColumnType::Custom(_) => Either4::A("ColumnType Custom not supported yet".to_string()), + ColumnType::Blob => Either4::A("ColumnType Blob not supported yet".to_string()), + ColumnType::Counter => Either4::A("ColumnType Counter not supported yet".to_string()), + ColumnType::List(_) => Either4::A("ColumnType List not supported yet".to_string()), + ColumnType::Map(_, _) => Either4::A("ColumnType Map not supported yet".to_string()), + ColumnType::Set(_) => Either4::A("ColumnType Set not supported yet".to_string()), + ColumnType::Time => Either4::A("ColumnType Time not supported yet".to_string()), + ColumnType::Timeuuid => Either4::A("ColumnType Timeuuid not supported yet".to_string()), + ColumnType::Tuple(_) => Either4::A("ColumnType Tuple not supported yet".to_string()), + ColumnType::Varint => Either4::A("ColumnType Varint not supported yet".to_string()), + _ => todo!(), + }), + None => None, } } fn parse_udt( udt: &[(String, Option)], field_types: &[(String, ColumnType)], - ) -> serde_json::Value { - let mut result = serde_json::Map::new(); + ) -> HashMap> { + let mut result: HashMap> = HashMap::new(); for (i, (field_name, field_value)) in udt.iter().enumerate() { let field_type = &field_types[i].1; let parsed_value = Self::parse_value(field_value, field_type); - result.insert(field_name.clone(), parsed_value); + if let Some(parsed_value) = parsed_value { + result.insert(field_name.clone(), parsed_value); + } } - serde_json::Value::Object(result) + result } } diff --git a/src/session/scylla_session.rs b/src/session/scylla_session.rs index 5b9e215..028024f 100644 --- a/src/session/scylla_session.rs +++ b/src/session/scylla_session.rs @@ -6,7 +6,7 @@ use crate::query::batch_statement::ScyllaBatchStatement; use crate::query::scylla_prepared_statement::PreparedStatement; use crate::query::scylla_query::Query; use crate::types::uuid::Uuid; -use napi::bindgen_prelude::{Either3, Either4}; +use napi::bindgen_prelude::{BigInt, Either3, Either4}; use napi::Either; use super::metrics; @@ -68,7 +68,7 @@ impl ScyllaSession { Vec>>>, >, options: Option, - ) -> napi::Result { + ) -> napi::Result>>> { let values = QueryParameter::parser(parameters.clone()).ok_or_else(|| { napi::Error::new( napi::Status::InvalidArg, @@ -118,7 +118,7 @@ impl ScyllaSession { prepared: &scylla::prepared_statement::PreparedStatement, values: QueryParameter<'_>, query: &str, - ) -> napi::Result { + ) -> napi::Result>>> { let query_result = self.session.execute(prepared, values).await.map_err(|e| { napi::Error::new( napi::Status::InvalidArg, @@ -136,7 +136,7 @@ impl ScyllaSession { &self, query: Either, values: QueryParameter<'_>, - ) -> napi::Result { + ) -> napi::Result>>> { let query_result = match &query { Either::A(query_str) => self.session.query(query_str.clone(), values).await, Either::B(query_ref) => self.session.query(query_ref.clone(), values).await, @@ -166,7 +166,7 @@ impl ScyllaSession { parameters: Option< Vec>>>, >, - ) -> napi::Result { + ) -> napi::Result>>> { let values = QueryParameter::parser(parameters.clone()).ok_or(napi::Error::new( napi::Status::InvalidArg, format!("Something went wrong with your query parameters. {parameters:?}"), @@ -241,7 +241,7 @@ impl ScyllaSession { parameters: Vec< Option>>>>, >, - ) -> napi::Result { + ) -> napi::Result>>> { let values = parameters .iter() .map(|params| { From f3cd493f9c8f39cb11b1b2cd29aac79c160335ae Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Tue, 27 Aug 2024 16:24:38 -0300 Subject: [PATCH 02/13] feat(types): add support for Decimal and Duration types This commit introduces support for Decimal and Duration types in the Scylla driver. It also updates the query parameter and result parsing to handle these new types. The changes are reflected in the TypeScript definitions and JavaScript exports as well. BREAKING CHANGE: The query parameter and result types have been updated to accommodate the new Decimal and Duration types. This may require changes in the calling code if it was relying on the previous types. Signed-off-by: Daniel Boll --- Cargo.toml | 2 +- examples/refactor-test.mts | 12 +- index.d.ts | 195 +++++++++++++++++++++------------ index.js | 3 +- src/helpers/query_parameter.rs | 46 ++++++-- src/helpers/query_results.rs | 193 +++++++++++++++++++++----------- src/session/scylla_session.rs | 39 ++++--- src/types/decimal.rs | 27 +++++ src/types/duration.rs | 29 +++++ src/types/mod.rs | 2 + src/types/uuid.rs | 9 ++ 11 files changed, 389 insertions(+), 168 deletions(-) create mode 100644 src/types/decimal.rs create mode 100644 src/types/duration.rs diff --git a/Cargo.toml b/Cargo.toml index 341ab60..602e30c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,4 @@ openssl = { version = "0.10", features = ["vendored"] } napi-build = "2.0.1" [profile.release] -lto = true +lto = true \ No newline at end of file diff --git a/examples/refactor-test.mts b/examples/refactor-test.mts index 71b7ccb..dc94301 100644 --- a/examples/refactor-test.mts +++ b/examples/refactor-test.mts @@ -1,4 +1,4 @@ -import { Cluster } from "../index.js"; +import { Cluster, Uuid } from "../index.js"; const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; @@ -16,4 +16,12 @@ await session.execute( "CREATE TABLE IF NOT EXISTS refactor (a text, b int, c uuid, d bigint, primary key (a))", ); -await session.execute("INSERT INTO basic (a, b, c) VALUES (1, 2, 'abc')"); +await session.execute("INSERT INTO refactor (a, b, c, d) VALUES (?, ?, ?, ?)", [ + "is a string", + 42, + Uuid.randomV4(), + 42192219n, +]); + +const results = await session.execute("SELECT * FROM refactor"); +console.log(results); diff --git a/index.d.ts b/index.d.ts index c675e97..f672419 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,19 +6,19 @@ export const enum Compression { None = 0, Lz4 = 1, - Snappy = 2 + Snappy = 2, } export interface ClusterConfig { - nodes: Array - compression?: Compression - defaultExecutionProfile?: ExecutionProfile - keyspace?: string - auth?: Auth - ssl?: Ssl + nodes: Array; + compression?: Compression; + defaultExecutionProfile?: ExecutionProfile; + keyspace?: string; + auth?: Auth; + ssl?: Ssl; /** The driver automatically awaits schema agreement after a schema-altering query is executed. Waiting for schema agreement more than necessary is never a bug, but might slow down applications which do a lot of schema changes (e.g. a migration). For instance, in case where somebody wishes to create a keyspace and then a lot of tables in it, it makes sense only to wait after creating a keyspace and after creating all the tables rather than after every query. */ - autoAwaitSchemaAgreement?: boolean + autoAwaitSchemaAgreement?: boolean; /** If the schema is not agreed upon, the driver sleeps for a duration in seconds before checking it again. The default value is 0.2 (200 milliseconds) */ - schemaAgreementInterval?: number + schemaAgreementInterval?: number; } export const enum Consistency { Any = 0, @@ -31,70 +31,75 @@ export const enum Consistency { EachQuorum = 7, LocalOne = 10, Serial = 8, - LocalSerial = 9 + LocalSerial = 9, } export const enum SerialConsistency { Serial = 8, - LocalSerial = 9 + LocalSerial = 9, } export interface ExecutionProfile { - consistency?: Consistency - serialConsistency?: SerialConsistency - requestTimeout?: number + consistency?: Consistency; + serialConsistency?: SerialConsistency; + requestTimeout?: number; } export interface ConnectionOptions { - keyspace?: string - auth?: Auth - ssl?: Ssl + keyspace?: string; + auth?: Auth; + ssl?: Ssl; } export interface Auth { - username: string - password: string + username: string; + password: string; } export interface Ssl { - enabled: boolean - caFilepath?: string - privateKeyFilepath?: string - truststoreFilepath?: string - verifyMode?: VerifyMode + enabled: boolean; + caFilepath?: string; + privateKeyFilepath?: string; + truststoreFilepath?: string; + verifyMode?: VerifyMode; } export const enum VerifyMode { None = 0, - Peer = 1 + Peer = 1, } export interface QueryOptions { - prepare?: boolean + prepare?: boolean; } export interface ScyllaKeyspace { - strategy: ScyllaStrategy - tables: Record - views: Record + strategy: ScyllaStrategy; + tables: Record; + views: Record; } export interface ScyllaStrategy { - kind: string - data?: SimpleStrategy | NetworkTopologyStrategy | Other + kind: string; + data?: SimpleStrategy | NetworkTopologyStrategy | Other; } export interface SimpleStrategy { - replicationFactor: number + replicationFactor: number; } export interface NetworkTopologyStrategy { - datacenterRepfactors: Record + datacenterRepfactors: Record; } export interface Other { - name: string - data: Record + name: string; + data: Record; } export interface ScyllaTable { - columns: Array - partitionKey: Array - clusteringKey: Array - partitioner?: string + columns: Array; + partitionKey: Array; + clusteringKey: Array; + partitioner?: string; } export interface ScyllaMaterializedView { - viewMetadata: ScyllaTable - baseTableName: string + viewMetadata: ScyllaTable; + baseTableName: string; } -export type ScyllaCluster = Cluster +export interface Duration { + months: number; + days: number; + nanoseconds: number; +} +export type ScyllaCluster = Cluster; export class Cluster { /** * Object config is in the format: @@ -102,11 +107,14 @@ export class Cluster { * nodes: Array, * } */ - constructor(clusterConfig: ClusterConfig) + constructor(clusterConfig: ClusterConfig); /** Connect to the cluster */ - connect(keyspaceOrOptions?: string | ConnectionOptions | undefined | null, options?: ConnectionOptions | undefined | null): Promise + connect( + keyspaceOrOptions?: string | ConnectionOptions | undefined | null, + options?: ConnectionOptions | undefined | null, + ): Promise; } -export type ScyllaBatchStatement = BatchStatement +export type ScyllaBatchStatement = BatchStatement; /** * Batch statements * @@ -115,36 +123,36 @@ export type ScyllaBatchStatement = BatchStatement * Only INSERT, UPDATE and DELETE statements are allowed. */ export class BatchStatement { - constructor() + constructor(); /** * Appends a statement to the batch. * * _Warning_ * Using simple statements with bind markers in batches is strongly discouraged. For each simple statement with a non-empty list of values in the batch, the driver will send a prepare request, and it will be done sequentially. Results of preparation are not cached between `session.batch` calls. Consider preparing the statements before putting them into the batch. */ - appendStatement(statement: Query | PreparedStatement): void + appendStatement(statement: Query | PreparedStatement): void; } export class PreparedStatement { - setConsistency(consistency: Consistency): void - setSerialConsistency(serialConsistency: SerialConsistency): void + setConsistency(consistency: Consistency): void; + setSerialConsistency(serialConsistency: SerialConsistency): void; } export class Query { - constructor(query: string) - setConsistency(consistency: Consistency): void - setSerialConsistency(serialConsistency: SerialConsistency): void - setPageSize(pageSize: number): void + constructor(query: string); + setConsistency(consistency: Consistency): void; + setSerialConsistency(serialConsistency: SerialConsistency): void; + setPageSize(pageSize: number): void; } export class Metrics { /** Returns counter for nonpaged queries */ - getQueriesNum(): bigint + getQueriesNum(): bigint; /** Returns counter for pages requested in paged queries */ - getQueriesIterNum(): bigint + getQueriesIterNum(): bigint; /** Returns counter for errors occurred in nonpaged queries */ - getErrorsNum(): bigint + getErrorsNum(): bigint; /** Returns counter for errors occurred in paged queries */ - getErrorsIterNum(): bigint + getErrorsIterNum(): bigint; /** Returns average latency in milliseconds */ - getLatencyAvgMs(): bigint + getLatencyAvgMs(): bigint; /** * Returns latency from histogram for a given percentile * @@ -152,11 +160,12 @@ export class Metrics { * * * `percentile` - float value (0.0 - 100.0), value will be clamped to this range */ - getLatencyPercentileMs(percentile: number): bigint + getLatencyPercentileMs(percentile: number): bigint; } +type JSQueryResult = any; export class ScyllaSession { - metrics(): Metrics - getClusterData(): Promise + metrics(): Metrics; + getClusterData(): Promise; /** * Sends a query to the database and receives a response.\ * Returns only a single page of results, to receive multiple pages use (TODO: Not implemented yet) @@ -173,9 +182,34 @@ export class ScyllaSession { * driver does not check it by itself, so incorrect data will be written if the order is * wrong. */ - execute(query: string | Query | PreparedStatement, parameters?: Array> | undefined | null, options?: QueryOptions | undefined | null): Promise>> - query(scyllaQuery: Query, parameters?: Array> | undefined | null): Promise>> - prepare(query: string): Promise + execute( + query: string | Query | PreparedStatement, + parameters?: + | Array< + | number + | string + | Uuid + | bigint + | Record + > + | undefined + | null, + options?: QueryOptions | undefined | null, + ): Promise; + query( + scyllaQuery: Query, + parameters?: + | Array< + | number + | string + | Uuid + | bigint + | Record + > + | undefined + | null, + ): Promise; + prepare(query: string): Promise; /** * Perform a batch query\ * Batch contains many `simple` or `prepared` queries which are executed at once\ @@ -213,7 +247,20 @@ export class ScyllaSession { * console.log(await session.execute("SELECT * FROM users")); * ``` */ - batch(batch: BatchStatement, parameters: Array> | undefined | null>): Promise>> + batch( + batch: BatchStatement, + parameters: Array< + | Array< + | number + | string + | Uuid + | bigint + | Record + > + | undefined + | null + >, + ): Promise; /** * Sends `USE ` request on all connections\ * This allows to write `SELECT * FROM table` instead of `SELECT * FROM keyspace.table`\ @@ -253,7 +300,10 @@ export class ScyllaSession { * .catch(console.error); * ``` */ - useKeyspace(keyspaceName: string, caseSensitive?: boolean | undefined | null): Promise + useKeyspace( + keyspaceName: string, + caseSensitive?: boolean | undefined | null, + ): Promise; /** * session.awaitSchemaAgreement returns a Promise that can be awaited as long as schema is not in an agreement. * However, it won’t wait forever; ClusterConfig defines a timeout that limits the time of waiting. If the timeout elapses, @@ -280,21 +330,22 @@ export class ScyllaSession { * console.log(isAgreed); * ``` */ - awaitSchemaAgreement(): Promise - checkSchemaAgreement(): Promise + awaitSchemaAgreement(): Promise; + checkSchemaAgreement(): Promise; } export class ScyllaClusterData { /** * Access keyspaces details collected by the driver Driver collects various schema details like * tables, partitioners, columns, types. They can be read using this method */ - getKeyspaceInfo(): Record | null + getKeyspaceInfo(): Record | null; } +export class Decimal {} export class Uuid { /** Generates a random UUID v4. */ - static randomV4(): Uuid + static randomV4(): Uuid; /** Parses a UUID from a string. It may fail if the string is not a valid UUID. */ - static fromString(str: string): Uuid + static fromString(str: string): Uuid; /** Returns the string representation of the UUID. */ - toString(): string + toString(): string; } diff --git a/index.js b/index.js index 38f933a..6b29ad0 100644 --- a/index.js +++ b/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Uuid } = nativeBinding +const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Uuid } = nativeBinding module.exports.Compression = Compression module.exports.Consistency = Consistency @@ -323,6 +323,7 @@ module.exports.Query = Query module.exports.Metrics = Metrics module.exports.ScyllaSession = ScyllaSession module.exports.ScyllaClusterData = ScyllaClusterData +module.exports.Decimal = Decimal module.exports.Uuid = Uuid const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') diff --git a/src/helpers/query_parameter.rs b/src/helpers/query_parameter.rs index f347e1c..b14cfbd 100644 --- a/src/helpers/query_parameter.rs +++ b/src/helpers/query_parameter.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use crate::types::uuid::Uuid; -use napi::bindgen_prelude::{Either3, Either4}; +use napi::bindgen_prelude::{BigInt, Either4, Either5}; use scylla::{ frame::response::result::CqlValue, serialize::{ @@ -13,8 +13,17 @@ use scylla::{ pub struct QueryParameter<'a> { #[allow(clippy::type_complexity)] - pub(crate) parameters: - Option>>>>, + pub(crate) parameters: Option< + Vec< + Either5< + u32, + String, + &'a Uuid, + BigInt, + HashMap>, + >, + >, + >, } impl<'a> SerializeRow for QueryParameter<'a> { @@ -26,19 +35,23 @@ impl<'a> SerializeRow for QueryParameter<'a> { if let Some(parameters) = &self.parameters { for (i, parameter) in parameters.iter().enumerate() { match parameter { - Either4::A(num) => { + Either5::A(num) => { CqlValue::Int(*num as i32) .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } - Either4::B(str) => { + Either5::B(str) => { CqlValue::Text(str.to_string()) .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } - Either4::C(uuid) => { + Either5::C(uuid) => { CqlValue::Uuid(uuid.get_inner()) .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } - Either4::D(map) => { + Either5::D(bigint) => { + CqlValue::BigInt(bigint.get_i64().0) + .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; + } + Either5::E(map) => { CqlValue::UserDefinedType { // FIXME: I'm not sure why this is even necessary tho, but if it's and makes sense we'll have to make it so we get the correct info keyspace: "keyspace".to_string(), @@ -46,9 +59,12 @@ impl<'a> SerializeRow for QueryParameter<'a> { fields: map .iter() .map(|(key, value)| match value { - Either3::A(num) => (key.to_string(), Some(CqlValue::Int(*num as i32))), - Either3::B(str) => (key.to_string(), Some(CqlValue::Text(str.to_string()))), - Either3::C(uuid) => (key.to_string(), Some(CqlValue::Uuid(uuid.get_inner()))), + Either4::A(num) => (key.to_string(), Some(CqlValue::Int(*num as i32))), + Either4::B(str) => (key.to_string(), Some(CqlValue::Text(str.to_string()))), + Either4::C(uuid) => (key.to_string(), Some(CqlValue::Uuid(uuid.get_inner()))), + Either4::D(bigint) => { + (key.to_string(), Some(CqlValue::BigInt(bigint.get_i64().0))) + } }) .collect::)>>(), } @@ -69,7 +85,15 @@ impl<'a> QueryParameter<'a> { #[allow(clippy::type_complexity)] pub fn parser( parameters: Option< - Vec>>>, + Vec< + Either5< + u32, + String, + &'a Uuid, + BigInt, + HashMap>, + >, + >, >, ) -> Option { if parameters.is_none() { diff --git a/src/helpers/query_results.rs b/src/helpers/query_results.rs index 0c336c3..a9fc24c 100644 --- a/src/helpers/query_results.rs +++ b/src/helpers/query_results.rs @@ -1,32 +1,50 @@ use std::collections::HashMap; -use napi::bindgen_prelude::{BigInt, Either4}; +use napi::bindgen_prelude::{BigInt, Either10, Either9}; use scylla::frame::response::result::{ColumnType, CqlValue}; -use crate::types::uuid::Uuid; +use crate::types::{decimal::Decimal, duration::Duration, uuid::Uuid}; pub struct QueryResult { pub(crate) result: scylla::QueryResult, } +macro_rules! define_return_type { + ($($t:ty),+) => { + type NativeTypes = Either9<$($t),+>; + type WithMapType = Either10<$($t),+, HashMap>; + type ReturnType = napi::Result>; + pub type JSQueryResult = napi::Result>>; + }; +} +define_return_type!( + String, + i64, + f64, + bool, + BigInt, + Uuid, + Duration, + Decimal, + Vec +); + impl QueryResult { - pub fn parser( - result: scylla::QueryResult, - ) -> Vec>> { + pub fn parser(result: scylla::QueryResult) -> JSQueryResult { if result.result_not_rows().is_ok() || result.rows.is_none() { - return Default::default(); + return Ok(Default::default()); } let rows = result.rows.unwrap(); let column_specs = result.col_specs; - let mut result_json: Vec>> = vec![]; + let mut result_json: Vec> = vec![]; for row in rows { - let mut row_object: HashMap> = HashMap::new(); + let mut row_object: HashMap = HashMap::new(); for (i, column) in row.columns.iter().enumerate() { let column_name = column_specs[i].name.clone(); - let column_value = Self::parse_value(column, &column_specs[i].typ); + let column_value = Self::parse_value(column, &column_specs[i].typ)?; if let Some(column_value) = column_value { row_object.insert(column_name, column_value); } @@ -35,77 +53,122 @@ impl QueryResult { result_json.push(row_object); } - result_json + Ok(result_json) } - fn parse_value( - column: &Option, - column_type: &ColumnType, - ) -> Option> { - match column { - Some(column) => Some(match column_type { - ColumnType::Ascii => Either4::A(column.as_ascii().unwrap().to_string()), - ColumnType::Text => Either4::A(column.as_text().unwrap().to_string()), - ColumnType::Uuid => Either4::D(Uuid { + fn parse_value(column: &Option, column_type: &ColumnType) -> ReturnType { + column + .as_ref() + .map(|column| match column_type { + ColumnType::Ascii => Ok(WithMapType::A(column.as_ascii().unwrap().to_string())), + ColumnType::Text => Ok(WithMapType::A(column.as_text().unwrap().to_string())), + ColumnType::Uuid => Ok(WithMapType::F(Uuid { uuid: column.as_uuid().unwrap(), - }), - ColumnType::BigInt => Either4::C(column.as_bigint().unwrap().into()), - ColumnType::Int => Either4::B(column.as_int().unwrap()), - // ColumnType::Int => serde_json::Value::Number( - // serde_json::Number::from_f64(column.as_int().unwrap() as f64).unwrap(), - // ), - // ColumnType::Float => serde_json::Value::Number( - // serde_json::Number::from_f64(column.as_float().unwrap() as f64).unwrap(), - // ), - // ColumnType::Timestamp | ColumnType::Date => { - // serde_json::Value::String(column.as_cql_date().unwrap().0.to_string()) - // } - // ColumnType::UserDefinedType { field_types, .. } => { - // Self::parse_udt(column.as_udt().unwrap(), field_types) - // } - // ColumnType::Boolean => serde_json::Value::Bool(column.as_boolean().unwrap()), - // ColumnType::Inet => serde_json::Value::String(column.as_inet().unwrap().to_string()), - // ColumnType::Double => serde_json::Value::Number( - // serde_json::Number::from_f64(column.as_double().unwrap()).unwrap(), - // ), - // ColumnType::SmallInt => serde_json::Value::Number( - // serde_json::Number::from_f64(column.as_smallint().unwrap() as f64).unwrap(), - // ), - // ColumnType::TinyInt => serde_json::Value::Number( - // serde_json::Number::from_f64(column.as_tinyint().unwrap() as f64).unwrap(), - // ), - ColumnType::Decimal => Either4::A("ColumnType Decimal not supported yet".to_string()), - ColumnType::Duration => Either4::A("ColumnType Duration not supported yet".to_string()), - ColumnType::Custom(_) => Either4::A("ColumnType Custom not supported yet".to_string()), - ColumnType::Blob => Either4::A("ColumnType Blob not supported yet".to_string()), - ColumnType::Counter => Either4::A("ColumnType Counter not supported yet".to_string()), - ColumnType::List(_) => Either4::A("ColumnType List not supported yet".to_string()), - ColumnType::Map(_, _) => Either4::A("ColumnType Map not supported yet".to_string()), - ColumnType::Set(_) => Either4::A("ColumnType Set not supported yet".to_string()), - ColumnType::Time => Either4::A("ColumnType Time not supported yet".to_string()), - ColumnType::Timeuuid => Either4::A("ColumnType Timeuuid not supported yet".to_string()), - ColumnType::Tuple(_) => Either4::A("ColumnType Tuple not supported yet".to_string()), - ColumnType::Varint => Either4::A("ColumnType Varint not supported yet".to_string()), - _ => todo!(), - }), - None => None, - } + })), + ColumnType::BigInt => Ok(WithMapType::E(column.as_bigint().unwrap().into())), + ColumnType::Int => Ok(WithMapType::B(column.as_int().unwrap() as i64)), + ColumnType::Float => Ok(WithMapType::C(column.as_float().unwrap() as f64)), + ColumnType::Double => Ok(WithMapType::C(column.as_double().unwrap())), + ColumnType::Boolean => Ok(WithMapType::D(column.as_boolean().unwrap())), + ColumnType::SmallInt => Ok(WithMapType::B(column.as_smallint().unwrap() as i64)), + ColumnType::TinyInt => Ok(WithMapType::B(column.as_tinyint().unwrap() as i64)), + ColumnType::Date | ColumnType::Timestamp => { + Ok(WithMapType::A(column.as_date().unwrap().to_string())) + } + ColumnType::Inet => Ok(WithMapType::A(column.as_inet().unwrap().to_string())), + ColumnType::Duration => Ok(WithMapType::G(column.as_cql_duration().unwrap().into())), + ColumnType::Decimal => Ok(WithMapType::H( + column.clone().into_cql_decimal().unwrap().into(), + )), + ColumnType::Blob => Ok(WithMapType::I(column.as_blob().unwrap().clone())), + ColumnType::Counter => Ok(WithMapType::B(column.as_counter().unwrap().0)), + ColumnType::Varint => Ok(WithMapType::I( + column + .clone() + .into_cql_varint() + .unwrap() + .as_signed_bytes_be_slice() + .into(), + )), + ColumnType::Time => Ok(WithMapType::B(column.as_time().unwrap().nanosecond() as i64)), + ColumnType::Timeuuid => Ok(WithMapType::F(column.as_timeuuid().unwrap().into())), + ColumnType::Map(key, value) => { + let map = column + .as_map() + .unwrap() + .iter() + .map(|(k, v)| { + let key = Self::parse_value(&Some(k.clone()), key).unwrap(); + let value = + Self::remove_map_from_type(Self::parse_value(&Some(v.clone()), value).unwrap())? + .unwrap(); + key + .map(|key| match key { + WithMapType::A(key) => Ok((key, value)), + _ => Err(napi::Error::new( + napi::Status::GenericFailure, + "Map key must be a string", + )), + }) + .transpose() + }) + .collect::>>>(); + + Ok(WithMapType::J(map?.unwrap())) + } + ColumnType::UserDefinedType { field_types, .. } => Ok(WithMapType::J(Self::parse_udt( + column.as_udt().unwrap(), + field_types, + )?)), + ColumnType::Custom(_) => Ok(WithMapType::A( + "ColumnType Custom not supported yet".to_string(), + )), + ColumnType::List(_) => Ok(WithMapType::A( + "ColumnType List not supported yet".to_string(), + )), + ColumnType::Set(_) => Ok(WithMapType::A( + "ColumnType Set not supported yet".to_string(), + )), + ColumnType::Tuple(_) => Ok(WithMapType::A( + "ColumnType Tuple not supported yet".to_string(), + )), + }) + .transpose() } fn parse_udt( udt: &[(String, Option)], field_types: &[(String, ColumnType)], - ) -> HashMap> { - let mut result: HashMap> = HashMap::new(); + ) -> napi::Result> { + let mut result: HashMap = HashMap::new(); for (i, (field_name, field_value)) in udt.iter().enumerate() { let field_type = &field_types[i].1; let parsed_value = Self::parse_value(field_value, field_type); - if let Some(parsed_value) = parsed_value { + if let Some(parsed_value) = Self::remove_map_from_type(parsed_value?)? { result.insert(field_name.clone(), parsed_value); } } - result + Ok(result) + } + + fn remove_map_from_type(a: Option) -> napi::Result> { + a.map(|f| match f { + WithMapType::A(a) => Ok(NativeTypes::A(a)), + WithMapType::B(a) => Ok(NativeTypes::B(a)), + WithMapType::C(a) => Ok(NativeTypes::C(a)), + WithMapType::D(a) => Ok(NativeTypes::D(a)), + WithMapType::E(a) => Ok(NativeTypes::E(a)), + WithMapType::F(a) => Ok(NativeTypes::F(a)), + WithMapType::G(a) => Ok(NativeTypes::G(a)), + WithMapType::H(a) => Ok(NativeTypes::H(a)), + WithMapType::I(a) => Ok(NativeTypes::I(a)), + WithMapType::J(_) => Err(napi::Error::new( + napi::Status::GenericFailure, + "Map type is not supported in this context".to_string(), + )), + }) + .transpose() } } diff --git a/src/session/scylla_session.rs b/src/session/scylla_session.rs index 028024f..6f26e72 100644 --- a/src/session/scylla_session.rs +++ b/src/session/scylla_session.rs @@ -1,13 +1,12 @@ -use std::collections::HashMap; - use crate::helpers::query_parameter::QueryParameter; -use crate::helpers::query_results::QueryResult; +use crate::helpers::query_results::{JSQueryResult, QueryResult}; use crate::query::batch_statement::ScyllaBatchStatement; use crate::query::scylla_prepared_statement::PreparedStatement; use crate::query::scylla_query::Query; use crate::types::uuid::Uuid; -use napi::bindgen_prelude::{BigInt, Either3, Either4}; +use napi::bindgen_prelude::{BigInt, Either3, Either4, Either5}; use napi::Either; +use std::collections::HashMap; use super::metrics; use super::topology::ScyllaClusterData; @@ -65,10 +64,12 @@ impl ScyllaSession { &self, query: Either3, parameters: Option< - Vec>>>, + Vec< + Either5>>, + >, >, options: Option, - ) -> napi::Result>>> { + ) -> JSQueryResult { let values = QueryParameter::parser(parameters.clone()).ok_or_else(|| { napi::Error::new( napi::Status::InvalidArg, @@ -118,7 +119,7 @@ impl ScyllaSession { prepared: &scylla::prepared_statement::PreparedStatement, values: QueryParameter<'_>, query: &str, - ) -> napi::Result>>> { + ) -> JSQueryResult { let query_result = self.session.execute(prepared, values).await.map_err(|e| { napi::Error::new( napi::Status::InvalidArg, @@ -128,7 +129,7 @@ impl ScyllaSession { ), ) })?; - Ok(QueryResult::parser(query_result)) + QueryResult::parser(query_result) } // Helper method to handle direct queries @@ -136,7 +137,7 @@ impl ScyllaSession { &self, query: Either, values: QueryParameter<'_>, - ) -> napi::Result>>> { + ) -> JSQueryResult { let query_result = match &query { Either::A(query_str) => self.session.query(query_str.clone(), values).await, Either::B(query_ref) => self.session.query(query_ref.clone(), values).await, @@ -155,7 +156,7 @@ impl ScyllaSession { ) })?; - Ok(QueryResult::parser(query_result)) + QueryResult::parser(query_result) } #[allow(clippy::type_complexity)] @@ -164,9 +165,11 @@ impl ScyllaSession { &self, scylla_query: &Query, parameters: Option< - Vec>>>, + Vec< + Either5>>, + >, >, - ) -> napi::Result>>> { + ) -> JSQueryResult { let values = QueryParameter::parser(parameters.clone()).ok_or(napi::Error::new( napi::Status::InvalidArg, format!("Something went wrong with your query parameters. {parameters:?}"), @@ -183,7 +186,7 @@ impl ScyllaSession { ) })?; - Ok(QueryResult::parser(query_result)) + QueryResult::parser(query_result) } #[napi] @@ -239,9 +242,13 @@ impl ScyllaSession { &self, batch: &ScyllaBatchStatement, parameters: Vec< - Option>>>>, + Option< + Vec< + Either5>>, + >, + >, >, - ) -> napi::Result>>> { + ) -> JSQueryResult { let values = parameters .iter() .map(|params| { @@ -263,7 +270,7 @@ impl ScyllaSession { ) })?; - Ok(QueryResult::parser(query_result)) + QueryResult::parser(query_result) } /// Sends `USE ` request on all connections\ diff --git a/src/types/decimal.rs b/src/types/decimal.rs new file mode 100644 index 0000000..0c2e0d5 --- /dev/null +++ b/src/types/decimal.rs @@ -0,0 +1,27 @@ +use napi::bindgen_prelude::Uint8Array; +use scylla::frame::value::CqlDecimal; + +#[napi] +pub struct Decimal { + int_val: Uint8Array, + scale: i32, +} + +impl From for Decimal { + fn from(value: CqlDecimal) -> Self { + let (int_val, scale) = value.as_signed_be_bytes_slice_and_exponent(); + + Self { + int_val: int_val.into(), + scale, + } + } +} + +impl From for CqlDecimal { + fn from(value: Decimal) -> Self { + CqlDecimal::from_signed_be_bytes_slice_and_exponent(value.int_val.as_ref(), value.scale) + } +} + +// TODO: implement operations for this wrapper diff --git a/src/types/duration.rs b/src/types/duration.rs new file mode 100644 index 0000000..f838eb5 --- /dev/null +++ b/src/types/duration.rs @@ -0,0 +1,29 @@ +use scylla::frame::value::CqlDuration; + +#[napi(object)] +#[derive(Debug, Clone, Copy)] +pub struct Duration { + pub months: i32, + pub days: i32, + pub nanoseconds: i64, +} + +impl From for Duration { + fn from(value: CqlDuration) -> Self { + Self { + months: value.months, + days: value.days, + nanoseconds: value.nanoseconds, + } + } +} + +impl From for CqlDuration { + fn from(value: Duration) -> Self { + Self { + months: value.months, + days: value.days, + nanoseconds: value.nanoseconds, + } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index c8d27ed..8f2b505 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1 +1,3 @@ +pub mod decimal; +pub mod duration; pub mod uuid; diff --git a/src/types/uuid.rs b/src/types/uuid.rs index abe453b..8961962 100644 --- a/src/types/uuid.rs +++ b/src/types/uuid.rs @@ -1,4 +1,5 @@ use napi::Result; +use scylla::frame::value::CqlTimeuuid; #[napi()] #[derive(Debug, Clone, Copy)] @@ -18,6 +19,14 @@ impl From for uuid::Uuid { } } +impl From for Uuid { + fn from(uuid: CqlTimeuuid) -> Self { + Self { + uuid: *uuid.as_ref(), // NOTE: not sure if this is the best way + } + } +} + impl Uuid { pub(crate) fn get_inner(&self) -> uuid::Uuid { self.uuid From 4896761d1492118da9e3668369ccb9783a94d355 Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Fri, 27 Sep 2024 15:38:44 -0300 Subject: [PATCH 03/13] checkpoint Signed-off-by: Daniel Boll --- examples/refactor-test.mts | 13 ++++++- src/helpers/query_parameter.rs | 68 +++++++++++++++++----------------- src/helpers/query_results.rs | 1 + src/session/scylla_session.rs | 34 +++++++++++++++-- src/types/decimal.rs | 12 ++++++ src/types/duration.rs | 2 +- src/types/uuid.rs | 2 +- 7 files changed, 90 insertions(+), 42 deletions(-) diff --git a/examples/refactor-test.mts b/examples/refactor-test.mts index dc94301..4e1d81d 100644 --- a/examples/refactor-test.mts +++ b/examples/refactor-test.mts @@ -13,7 +13,18 @@ await session.execute( await session.useKeyspace("refactor"); await session.execute( - "CREATE TABLE IF NOT EXISTS refactor (a text, b int, c uuid, d bigint, primary key (a))", + `CREATE TABLE IF NOT EXISTS refactor ( + a int, + b float, + c boolean, + d date, + e duration, + f decimal, + g blob, + h varint, + i map, + primary key (a) + )`, ); await session.execute("INSERT INTO refactor (a, b, c, d) VALUES (?, ?, ?, ?)", [ diff --git a/src/helpers/query_parameter.rs b/src/helpers/query_parameter.rs index b14cfbd..c5494c7 100644 --- a/src/helpers/query_parameter.rs +++ b/src/helpers/query_parameter.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use crate::types::uuid::Uuid; -use napi::bindgen_prelude::{BigInt, Either4, Either5}; +use crate::types::{decimal::Decimal, duration::Duration, uuid::Uuid}; +use napi::bindgen_prelude::{BigInt, Either6, Either7}; use scylla::{ frame::response::result::CqlValue, serialize::{ @@ -11,19 +11,19 @@ use scylla::{ }, }; +macro_rules! define_expected_type { + ($lifetime:lifetime, $($t:ty),+) => { + pub type ParameterNativeTypes<$lifetime> = Either6<$($t),+>; + pub type ParameterWithMapType<$lifetime> = Either7<$($t),+, HashMap>>; + pub type JSQueryParameters<$lifetime> = napi::Result>>>; + }; +} + +define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal); + pub struct QueryParameter<'a> { #[allow(clippy::type_complexity)] - pub(crate) parameters: Option< - Vec< - Either5< - u32, - String, - &'a Uuid, - BigInt, - HashMap>, - >, - >, - >, + pub(crate) parameters: Option>>, } impl<'a> SerializeRow for QueryParameter<'a> { @@ -35,23 +35,25 @@ impl<'a> SerializeRow for QueryParameter<'a> { if let Some(parameters) = &self.parameters { for (i, parameter) in parameters.iter().enumerate() { match parameter { - Either5::A(num) => { + ParameterWithMapType::A(num) => { CqlValue::Int(*num as i32) .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } - Either5::B(str) => { + ParameterWithMapType::B(str) => { CqlValue::Text(str.to_string()) .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } - Either5::C(uuid) => { + ParameterWithMapType::C(uuid) => { CqlValue::Uuid(uuid.get_inner()) .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } - Either5::D(bigint) => { + ParameterWithMapType::D(bigint) => { CqlValue::BigInt(bigint.get_i64().0) .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } - Either5::E(map) => { + ParameterWithMapType::E(_duration) => todo!(), + ParameterWithMapType::F(_decimal) => todo!(), + ParameterWithMapType::G(map) => { CqlValue::UserDefinedType { // FIXME: I'm not sure why this is even necessary tho, but if it's and makes sense we'll have to make it so we get the correct info keyspace: "keyspace".to_string(), @@ -59,12 +61,20 @@ impl<'a> SerializeRow for QueryParameter<'a> { fields: map .iter() .map(|(key, value)| match value { - Either4::A(num) => (key.to_string(), Some(CqlValue::Int(*num as i32))), - Either4::B(str) => (key.to_string(), Some(CqlValue::Text(str.to_string()))), - Either4::C(uuid) => (key.to_string(), Some(CqlValue::Uuid(uuid.get_inner()))), - Either4::D(bigint) => { + ParameterNativeTypes::A(num) => { + (key.to_string(), Some(CqlValue::Int(*num as i32))) + } + ParameterNativeTypes::B(str) => { + (key.to_string(), Some(CqlValue::Text(str.to_string()))) + } + ParameterNativeTypes::C(uuid) => { + (key.to_string(), Some(CqlValue::Uuid(uuid.get_inner()))) + } + ParameterNativeTypes::D(bigint) => { (key.to_string(), Some(CqlValue::BigInt(bigint.get_i64().0))) } + ParameterNativeTypes::E(_duration) => todo!(), + ParameterNativeTypes::F(_decimal) => todo!(), }) .collect::)>>(), } @@ -83,19 +93,7 @@ impl<'a> SerializeRow for QueryParameter<'a> { impl<'a> QueryParameter<'a> { #[allow(clippy::type_complexity)] - pub fn parser( - parameters: Option< - Vec< - Either5< - u32, - String, - &'a Uuid, - BigInt, - HashMap>, - >, - >, - >, - ) -> Option { + pub fn parser(parameters: Option>>) -> Option { if parameters.is_none() { return Some(QueryParameter { parameters: None }); } diff --git a/src/helpers/query_results.rs b/src/helpers/query_results.rs index a9fc24c..a3a63c7 100644 --- a/src/helpers/query_results.rs +++ b/src/helpers/query_results.rs @@ -16,6 +16,7 @@ macro_rules! define_return_type { pub type JSQueryResult = napi::Result>>; }; } + define_return_type!( String, i64, diff --git a/src/session/scylla_session.rs b/src/session/scylla_session.rs index 6f26e72..fd38dff 100644 --- a/src/session/scylla_session.rs +++ b/src/session/scylla_session.rs @@ -3,8 +3,10 @@ use crate::helpers::query_results::{JSQueryResult, QueryResult}; use crate::query::batch_statement::ScyllaBatchStatement; use crate::query::scylla_prepared_statement::PreparedStatement; use crate::query::scylla_query::Query; +use crate::types::decimal::Decimal; +use crate::types::duration::Duration; use crate::types::uuid::Uuid; -use napi::bindgen_prelude::{BigInt, Either3, Either4, Either5}; +use napi::bindgen_prelude::{BigInt, Either3, Either6, Either7}; use napi::Either; use std::collections::HashMap; @@ -65,7 +67,15 @@ impl ScyllaSession { query: Either3, parameters: Option< Vec< - Either5>>, + Either7< + u32, + String, + &Uuid, + BigInt, + &Duration, + &Decimal, + HashMap>, + >, >, >, options: Option, @@ -166,7 +176,15 @@ impl ScyllaSession { scylla_query: &Query, parameters: Option< Vec< - Either5>>, + Either7< + u32, + String, + &Uuid, + BigInt, + &Duration, + &Decimal, + HashMap>, + >, >, >, ) -> JSQueryResult { @@ -244,7 +262,15 @@ impl ScyllaSession { parameters: Vec< Option< Vec< - Either5>>, + Either7< + u32, + String, + &Uuid, + BigInt, + &Duration, + &Decimal, + HashMap>, + >, >, >, >, diff --git a/src/types/decimal.rs b/src/types/decimal.rs index 0c2e0d5..66a900d 100644 --- a/src/types/decimal.rs +++ b/src/types/decimal.rs @@ -1,7 +1,10 @@ +use std::fmt::Debug; + use napi::bindgen_prelude::Uint8Array; use scylla::frame::value::CqlDecimal; #[napi] +#[derive(Clone)] pub struct Decimal { int_val: Uint8Array, scale: i32, @@ -24,4 +27,13 @@ impl From for CqlDecimal { } } +impl Debug for Decimal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Decimal") + .field("int_val", &self.int_val.into_iter().collect::>()) + .field("scale", &self.scale) + .finish() + } +} + // TODO: implement operations for this wrapper diff --git a/src/types/duration.rs b/src/types/duration.rs index f838eb5..b74f8e1 100644 --- a/src/types/duration.rs +++ b/src/types/duration.rs @@ -1,6 +1,6 @@ use scylla::frame::value::CqlDuration; -#[napi(object)] +#[napi] #[derive(Debug, Clone, Copy)] pub struct Duration { pub months: i32, diff --git a/src/types/uuid.rs b/src/types/uuid.rs index 8961962..e0abc16 100644 --- a/src/types/uuid.rs +++ b/src/types/uuid.rs @@ -1,7 +1,7 @@ use napi::Result; use scylla::frame::value::CqlTimeuuid; -#[napi()] +#[napi] #[derive(Debug, Clone, Copy)] pub struct Uuid { pub(crate) uuid: uuid::Uuid, From 63fbd10619dcaf03e2de5407467cc7273efa8358 Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Mon, 21 Oct 2024 23:37:55 -0300 Subject: [PATCH 04/13] feat(types): manage to insert and retrieve `Duration` and `Decimal` Floats, VarInts and Maps are not working currently. Also `Duration` and `Decimal` should have helper functions in their classes, but this will be implemented in another branch eventually. Signed-off-by: Daniel Boll --- examples/refactor-test.mts | 31 ++--- index.d.ts | 212 ++++++++++++++------------------- index.js | 9 +- scripts/fix-files.mjs | 37 ++++-- src/helpers/query_parameter.rs | 49 ++++++-- src/session/scylla_session.rs | 26 ++-- src/types/decimal.rs | 30 ++++- src/types/duration.rs | 23 ++++ 8 files changed, 244 insertions(+), 173 deletions(-) diff --git a/examples/refactor-test.mts b/examples/refactor-test.mts index 4e1d81d..b2d4a6b 100644 --- a/examples/refactor-test.mts +++ b/examples/refactor-test.mts @@ -1,4 +1,4 @@ -import { Cluster, Uuid } from "../index.js"; +import { Cluster, Uuid, Duration, Decimal } from "../index.js"; const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; @@ -15,24 +15,25 @@ await session.useKeyspace("refactor"); await session.execute( `CREATE TABLE IF NOT EXISTS refactor ( a int, - b float, - c boolean, - d date, - e duration, - f decimal, - g blob, - h varint, - i map, + b boolean, + c duration, + d decimal, + e blob, primary key (a) )`, ); +// TODO: varint, float, map -await session.execute("INSERT INTO refactor (a, b, c, d) VALUES (?, ?, ?, ?)", [ - "is a string", - 42, - Uuid.randomV4(), - 42192219n, -]); +// const a = await session +// .execute("INSERT INTO refactor (a, b, c, d, e) VALUES (?, ?, ?, ?, ?)", [ +// 1, +// true, +// new Duration(1, 1, 1), +// new Decimal([0x01, 0xe2, 0x40], 3), +// Buffer.from("hello").toJSON().data, +// ]) +// .catch(console.error); +// console.log(a); const results = await session.execute("SELECT * FROM refactor"); console.log(results); diff --git a/index.d.ts b/index.d.ts index f672419..a5d62a3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,19 +6,19 @@ export const enum Compression { None = 0, Lz4 = 1, - Snappy = 2, + Snappy = 2 } export interface ClusterConfig { - nodes: Array; - compression?: Compression; - defaultExecutionProfile?: ExecutionProfile; - keyspace?: string; - auth?: Auth; - ssl?: Ssl; + nodes: Array + compression?: Compression + defaultExecutionProfile?: ExecutionProfile + keyspace?: string + auth?: Auth + ssl?: Ssl /** The driver automatically awaits schema agreement after a schema-altering query is executed. Waiting for schema agreement more than necessary is never a bug, but might slow down applications which do a lot of schema changes (e.g. a migration). For instance, in case where somebody wishes to create a keyspace and then a lot of tables in it, it makes sense only to wait after creating a keyspace and after creating all the tables rather than after every query. */ - autoAwaitSchemaAgreement?: boolean; + autoAwaitSchemaAgreement?: boolean /** If the schema is not agreed upon, the driver sleeps for a duration in seconds before checking it again. The default value is 0.2 (200 milliseconds) */ - schemaAgreementInterval?: number; + schemaAgreementInterval?: number } export const enum Consistency { Any = 0, @@ -31,75 +31,70 @@ export const enum Consistency { EachQuorum = 7, LocalOne = 10, Serial = 8, - LocalSerial = 9, + LocalSerial = 9 } export const enum SerialConsistency { Serial = 8, - LocalSerial = 9, + LocalSerial = 9 } export interface ExecutionProfile { - consistency?: Consistency; - serialConsistency?: SerialConsistency; - requestTimeout?: number; + consistency?: Consistency + serialConsistency?: SerialConsistency + requestTimeout?: number } export interface ConnectionOptions { - keyspace?: string; - auth?: Auth; - ssl?: Ssl; + keyspace?: string + auth?: Auth + ssl?: Ssl } export interface Auth { - username: string; - password: string; + username: string + password: string } export interface Ssl { - enabled: boolean; - caFilepath?: string; - privateKeyFilepath?: string; - truststoreFilepath?: string; - verifyMode?: VerifyMode; + enabled: boolean + caFilepath?: string + privateKeyFilepath?: string + truststoreFilepath?: string + verifyMode?: VerifyMode } export const enum VerifyMode { None = 0, - Peer = 1, + Peer = 1 } export interface QueryOptions { - prepare?: boolean; + prepare?: boolean } export interface ScyllaKeyspace { - strategy: ScyllaStrategy; - tables: Record; - views: Record; + strategy: ScyllaStrategy + tables: Record + views: Record } export interface ScyllaStrategy { - kind: string; - data?: SimpleStrategy | NetworkTopologyStrategy | Other; + kind: string + data?: SimpleStrategy | NetworkTopologyStrategy | Other } export interface SimpleStrategy { - replicationFactor: number; + replicationFactor: number } export interface NetworkTopologyStrategy { - datacenterRepfactors: Record; + datacenterRepfactors: Record } export interface Other { - name: string; - data: Record; + name: string + data: Record } export interface ScyllaTable { - columns: Array; - partitionKey: Array; - clusteringKey: Array; - partitioner?: string; + columns: Array + partitionKey: Array + clusteringKey: Array + partitioner?: string } export interface ScyllaMaterializedView { - viewMetadata: ScyllaTable; - baseTableName: string; + viewMetadata: ScyllaTable + baseTableName: string } -export interface Duration { - months: number; - days: number; - nanoseconds: number; -} -export type ScyllaCluster = Cluster; +export type ScyllaCluster = Cluster export class Cluster { /** * Object config is in the format: @@ -107,14 +102,11 @@ export class Cluster { * nodes: Array, * } */ - constructor(clusterConfig: ClusterConfig); + constructor(clusterConfig: ClusterConfig) /** Connect to the cluster */ - connect( - keyspaceOrOptions?: string | ConnectionOptions | undefined | null, - options?: ConnectionOptions | undefined | null, - ): Promise; + connect(keyspaceOrOptions?: string | ConnectionOptions | undefined | null, options?: ConnectionOptions | undefined | null): Promise } -export type ScyllaBatchStatement = BatchStatement; +export type ScyllaBatchStatement = BatchStatement /** * Batch statements * @@ -123,36 +115,36 @@ export type ScyllaBatchStatement = BatchStatement; * Only INSERT, UPDATE and DELETE statements are allowed. */ export class BatchStatement { - constructor(); + constructor() /** * Appends a statement to the batch. * * _Warning_ * Using simple statements with bind markers in batches is strongly discouraged. For each simple statement with a non-empty list of values in the batch, the driver will send a prepare request, and it will be done sequentially. Results of preparation are not cached between `session.batch` calls. Consider preparing the statements before putting them into the batch. */ - appendStatement(statement: Query | PreparedStatement): void; + appendStatement(statement: Query | PreparedStatement): void } export class PreparedStatement { - setConsistency(consistency: Consistency): void; - setSerialConsistency(serialConsistency: SerialConsistency): void; + setConsistency(consistency: Consistency): void + setSerialConsistency(serialConsistency: SerialConsistency): void } export class Query { - constructor(query: string); - setConsistency(consistency: Consistency): void; - setSerialConsistency(serialConsistency: SerialConsistency): void; - setPageSize(pageSize: number): void; + constructor(query: string) + setConsistency(consistency: Consistency): void + setSerialConsistency(serialConsistency: SerialConsistency): void + setPageSize(pageSize: number): void } export class Metrics { /** Returns counter for nonpaged queries */ - getQueriesNum(): bigint; + getQueriesNum(): bigint /** Returns counter for pages requested in paged queries */ - getQueriesIterNum(): bigint; + getQueriesIterNum(): bigint /** Returns counter for errors occurred in nonpaged queries */ - getErrorsNum(): bigint; + getErrorsNum(): bigint /** Returns counter for errors occurred in paged queries */ - getErrorsIterNum(): bigint; + getErrorsIterNum(): bigint /** Returns average latency in milliseconds */ - getLatencyAvgMs(): bigint; + getLatencyAvgMs(): bigint /** * Returns latency from histogram for a given percentile * @@ -160,12 +152,11 @@ export class Metrics { * * * `percentile` - float value (0.0 - 100.0), value will be clamped to this range */ - getLatencyPercentileMs(percentile: number): bigint; + getLatencyPercentileMs(percentile: number): bigint } -type JSQueryResult = any; export class ScyllaSession { - metrics(): Metrics; - getClusterData(): Promise; + metrics(): Metrics + getClusterData(): Promise /** * Sends a query to the database and receives a response.\ * Returns only a single page of results, to receive multiple pages use (TODO: Not implemented yet) @@ -182,34 +173,9 @@ export class ScyllaSession { * driver does not check it by itself, so incorrect data will be written if the order is * wrong. */ - execute( - query: string | Query | PreparedStatement, - parameters?: - | Array< - | number - | string - | Uuid - | bigint - | Record - > - | undefined - | null, - options?: QueryOptions | undefined | null, - ): Promise; - query( - scyllaQuery: Query, - parameters?: - | Array< - | number - | string - | Uuid - | bigint - | Record - > - | undefined - | null, - ): Promise; - prepare(query: string): Promise; + execute(query: string | Query | PreparedStatement, parameters?: Array | Record>> | undefined | null, options?: QueryOptions | undefined | null): Promise + query(scyllaQuery: Query, parameters?: Array | Record>> | undefined | null): Promise + prepare(query: string): Promise /** * Perform a batch query\ * Batch contains many `simple` or `prepared` queries which are executed at once\ @@ -247,20 +213,7 @@ export class ScyllaSession { * console.log(await session.execute("SELECT * FROM users")); * ``` */ - batch( - batch: BatchStatement, - parameters: Array< - | Array< - | number - | string - | Uuid - | bigint - | Record - > - | undefined - | null - >, - ): Promise; + batch(batch: BatchStatement, parameters: Array | Record>> | undefined | null>): Promise /** * Sends `USE ` request on all connections\ * This allows to write `SELECT * FROM table` instead of `SELECT * FROM keyspace.table`\ @@ -300,10 +253,7 @@ export class ScyllaSession { * .catch(console.error); * ``` */ - useKeyspace( - keyspaceName: string, - caseSensitive?: boolean | undefined | null, - ): Promise; + useKeyspace(keyspaceName: string, caseSensitive?: boolean | undefined | null): Promise /** * session.awaitSchemaAgreement returns a Promise that can be awaited as long as schema is not in an agreement. * However, it won’t wait forever; ClusterConfig defines a timeout that limits the time of waiting. If the timeout elapses, @@ -330,22 +280,38 @@ export class ScyllaSession { * console.log(isAgreed); * ``` */ - awaitSchemaAgreement(): Promise; - checkSchemaAgreement(): Promise; + awaitSchemaAgreement(): Promise + checkSchemaAgreement(): Promise } export class ScyllaClusterData { /** * Access keyspaces details collected by the driver Driver collects various schema details like * tables, partitioners, columns, types. They can be read using this method */ - getKeyspaceInfo(): Record | null; + getKeyspaceInfo(): Record | null +} +export class Decimal { + constructor(intVal: Array, scale: number) + /** Returns the string representation of the Decimal. */ + toString(): string +} +export class Duration { + months: number + days: number + nanoseconds: number + constructor(months: number, days: number, nanoseconds: number) + /** Returns the string representation of the Duration. */ + toString(): string } -export class Decimal {} export class Uuid { /** Generates a random UUID v4. */ - static randomV4(): Uuid; + static randomV4(): Uuid /** Parses a UUID from a string. It may fail if the string is not a valid UUID. */ - static fromString(str: string): Uuid; + static fromString(str: string): Uuid /** Returns the string representation of the UUID. */ - toString(): string; + toString(): string } + +type NativeTypes = number | string | Uuid | bigint | Duration | Decimal; +type WithMapType = NativeTypes | Record; +type JSQueryResultType = Record[]; \ No newline at end of file diff --git a/index.js b/index.js index 6b29ad0..6822c84 100644 --- a/index.js +++ b/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Uuid } = nativeBinding +const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Uuid } = nativeBinding module.exports.Compression = Compression module.exports.Consistency = Consistency @@ -324,10 +324,11 @@ module.exports.Metrics = Metrics module.exports.ScyllaSession = ScyllaSession module.exports.ScyllaClusterData = ScyllaClusterData module.exports.Decimal = Decimal +module.exports.Duration = Duration module.exports.Uuid = Uuid const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') -Uuid.prototype[customInspectSymbol] = function () { - return this.toString(); -} \ No newline at end of file +Uuid.prototype[customInspectSymbol] = function () { return this.toString(); } +Duration.prototype[customInspectSymbol] = function () { return this.toString(); } +Decimal.prototype[customInspectSymbol] = function () { return this.toString(); } \ No newline at end of file diff --git a/scripts/fix-files.mjs b/scripts/fix-files.mjs index d6f03f3..1a4b995 100644 --- a/scripts/fix-files.mjs +++ b/scripts/fix-files.mjs @@ -2,16 +2,37 @@ import { readFileSync, writeFileSync } from "node:fs"; // Append to filename inspectors for custom types function addInspector(filename) { - writeFileSync(filename, readFileSync(filename, "utf8").concat(` + writeFileSync( + filename, + readFileSync(filename, "utf8") + .concat( + ` const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') -Uuid.prototype[customInspectSymbol] = function () { - return this.toString(); +Uuid.prototype[customInspectSymbol] = function () { return this.toString(); } +Duration.prototype[customInspectSymbol] = function () { return this.toString(); } +Decimal.prototype[customInspectSymbol] = function () { return this.toString(); } +`, + ) + .trim(), + ); } -`).trim()); + +function addJSQueryResultType(filename) { + writeFileSync( + filename, + readFileSync(filename, "utf8") + .concat( + ` +type NativeTypes = number | string | Uuid | bigint | Duration | Decimal; +type WithMapType = NativeTypes | Record; +type JSQueryResultType = Record[]; + `, + ) + .trim(), + ); } -let filename = process.argv[process.argv.length - 1] -if (filename.endsWith('index.js')) { - addInspector(filename) -} else if (filename.endsWith('index.d.ts')) { } +const filename = process.argv[process.argv.length - 1]; +if (filename.endsWith("index.js")) addInspector(filename); +else if (filename.endsWith("index.d.ts")) addJSQueryResultType(filename); diff --git a/src/helpers/query_parameter.rs b/src/helpers/query_parameter.rs index c5494c7..1a62fab 100644 --- a/src/helpers/query_parameter.rs +++ b/src/helpers/query_parameter.rs @@ -1,25 +1,25 @@ use std::collections::HashMap; use crate::types::{decimal::Decimal, duration::Duration, uuid::Uuid}; -use napi::bindgen_prelude::{BigInt, Either6, Either7}; +use napi::bindgen_prelude::{BigInt, Either8, Either9}; use scylla::{ frame::response::result::CqlValue, serialize::{ + RowWriter, SerializationError, row::{RowSerializationContext, SerializeRow}, value::SerializeCql, - RowWriter, SerializationError, }, }; macro_rules! define_expected_type { ($lifetime:lifetime, $($t:ty),+) => { - pub type ParameterNativeTypes<$lifetime> = Either6<$($t),+>; - pub type ParameterWithMapType<$lifetime> = Either7<$($t),+, HashMap>>; + pub type ParameterNativeTypes<$lifetime> = Either8<$($t),+>; + pub type ParameterWithMapType<$lifetime> = Either9<$($t),+, HashMap>>; pub type JSQueryParameters<$lifetime> = napi::Result>>>; }; } -define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal); +define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal, bool, Vec); pub struct QueryParameter<'a> { #[allow(clippy::type_complexity)] @@ -51,9 +51,22 @@ impl<'a> SerializeRow for QueryParameter<'a> { CqlValue::BigInt(bigint.get_i64().0) .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } - ParameterWithMapType::E(_duration) => todo!(), - ParameterWithMapType::F(_decimal) => todo!(), - ParameterWithMapType::G(map) => { + ParameterWithMapType::E(duration) => { + CqlValue::Duration((**duration).into()) + .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; + } + ParameterWithMapType::F(decimal) => { + CqlValue::Decimal((*decimal).into()) + .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; + } + ParameterWithMapType::G(bool) => { + CqlValue::Boolean(*bool).serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; + } + ParameterWithMapType::H(buffer) => { + CqlValue::Blob(u32_vec_to_u8_vec(buffer)) + .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; + } + ParameterWithMapType::I(map) => { CqlValue::UserDefinedType { // FIXME: I'm not sure why this is even necessary tho, but if it's and makes sense we'll have to make it so we get the correct info keyspace: "keyspace".to_string(), @@ -73,8 +86,20 @@ impl<'a> SerializeRow for QueryParameter<'a> { ParameterNativeTypes::D(bigint) => { (key.to_string(), Some(CqlValue::BigInt(bigint.get_i64().0))) } - ParameterNativeTypes::E(_duration) => todo!(), - ParameterNativeTypes::F(_decimal) => todo!(), + ParameterNativeTypes::E(duration) => ( + key.to_string(), + Some(CqlValue::Duration((**duration).into())), + ), + ParameterNativeTypes::F(decimal) => { + (key.to_string(), Some(CqlValue::Decimal((*decimal).into()))) + } + ParameterNativeTypes::G(bool) => { + (key.to_string(), Some(CqlValue::Boolean(*bool))) + } + ParameterNativeTypes::H(buffer) => ( + key.to_string(), + Some(CqlValue::Blob(u32_vec_to_u8_vec(buffer))), + ), }) .collect::)>>(), } @@ -110,3 +135,7 @@ impl<'a> QueryParameter<'a> { }) } } + +fn u32_vec_to_u8_vec(input: &[u32]) -> Vec { + input.iter().map(|&num| num as u8).collect() +} diff --git a/src/session/scylla_session.rs b/src/session/scylla_session.rs index fd38dff..e5b00ff 100644 --- a/src/session/scylla_session.rs +++ b/src/session/scylla_session.rs @@ -6,8 +6,8 @@ use crate::query::scylla_query::Query; use crate::types::decimal::Decimal; use crate::types::duration::Duration; use crate::types::uuid::Uuid; -use napi::bindgen_prelude::{BigInt, Either3, Either6, Either7}; use napi::Either; +use napi::bindgen_prelude::{BigInt, Either3, Either8, Either9}; use std::collections::HashMap; use super::metrics; @@ -67,14 +67,16 @@ impl ScyllaSession { query: Either3, parameters: Option< Vec< - Either7< + Either9< u32, String, &Uuid, BigInt, &Duration, &Decimal, - HashMap>, + bool, + Vec, + HashMap>>, >, >, >, @@ -176,14 +178,16 @@ impl ScyllaSession { scylla_query: &Query, parameters: Option< Vec< - Either7< + Either9< u32, String, &Uuid, BigInt, &Duration, &Decimal, - HashMap>, + bool, + Vec, + HashMap>>, >, >, >, @@ -200,7 +204,8 @@ impl ScyllaSession { .map_err(|e| { napi::Error::new( napi::Status::InvalidArg, - format!("Something went wrong with your query. - [{scylla_query}] - {parameters:?}\n{e}"), + // format!("Something went wrong with your query. - [{scylla_query}] - {parameters:?}\n{e}"), + format!("Something went wrong with your query. - [{scylla_query}] - TMP\n{e}"), ) })?; @@ -262,14 +267,19 @@ impl ScyllaSession { parameters: Vec< Option< Vec< - Either7< + Either9< u32, String, &Uuid, BigInt, &Duration, &Decimal, - HashMap>, + bool, + Vec, + HashMap< + String, + Either8>, + >, >, >, >, diff --git a/src/types/decimal.rs b/src/types/decimal.rs index 66a900d..da9dc45 100644 --- a/src/types/decimal.rs +++ b/src/types/decimal.rs @@ -1,12 +1,11 @@ use std::fmt::Debug; -use napi::bindgen_prelude::Uint8Array; use scylla::frame::value::CqlDecimal; #[napi] #[derive(Clone)] pub struct Decimal { - int_val: Uint8Array, + int_val: Vec, scale: i32, } @@ -21,8 +20,8 @@ impl From for Decimal { } } -impl From for CqlDecimal { - fn from(value: Decimal) -> Self { +impl From<&Decimal> for CqlDecimal { + fn from(value: &Decimal) -> Self { CqlDecimal::from_signed_be_bytes_slice_and_exponent(value.int_val.as_ref(), value.scale) } } @@ -30,10 +29,31 @@ impl From for CqlDecimal { impl Debug for Decimal { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Decimal") - .field("int_val", &self.int_val.into_iter().collect::>()) + .field("int_val", &self.int_val) .field("scale", &self.scale) .finish() } } // TODO: implement operations for this wrapper +#[napi] +impl Decimal { + #[napi(constructor)] + pub fn new(int_val: Vec, scale: i32) -> Self { + Self { int_val, scale } + } + + /// Returns the string representation of the Decimal. + // TODO: Check really how this is supposed to be displayed + #[napi] + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + let mut result = String::new(); + for b in &self.int_val { + result.push_str(&format!("{:02x}", b)); + } + result.push('e'); + result.push_str(&self.scale.to_string()); + result + } +} diff --git a/src/types/duration.rs b/src/types/duration.rs index b74f8e1..20eefaa 100644 --- a/src/types/duration.rs +++ b/src/types/duration.rs @@ -27,3 +27,26 @@ impl From for CqlDuration { } } } + +#[napi] +impl Duration { + #[napi(constructor)] + pub fn new(months: i32, days: i32, nanoseconds: i64) -> Self { + Self { + months, + days, + nanoseconds, + } + } + + /// Returns the string representation of the Duration. + // TODO: Check really how this is supposed to be displayed + #[napi] + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + format!( + "{} months, {} days, {} nanoseconds", + self.months, self.days, self.nanoseconds + ) + } +} From 6c6f9139cb7ee43a2ee3f22c682466e58ae8ecac Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Thu, 24 Oct 2024 19:26:56 -0300 Subject: [PATCH 05/13] feat(types): add types `float` and `varint` as custom types Signed-off-by: Daniel Boll --- Cargo.toml | 4 +- examples/custom-types/floats.mts | 22 ++++ examples/custom-types/list.mts | 0 examples/custom-types/set.mts | 0 examples/custom-types/uuid.mts | 0 examples/custom-types/varint.mts | 24 ++++ examples/refactor-test.mts | 30 +++-- index.d.ts | 35 +++++- index.js | 4 +- src/helpers/query_parameter.rs | 29 ++++- src/helpers/query_results.rs | 2 +- src/session/scylla_session.rs | 199 +++++++++++++++++-------------- src/types/float.rs | 35 ++++++ src/types/mod.rs | 2 + src/types/varint.rs | 48 ++++++++ 15 files changed, 319 insertions(+), 115 deletions(-) create mode 100644 examples/custom-types/floats.mts create mode 100644 examples/custom-types/list.mts create mode 100644 examples/custom-types/set.mts create mode 100644 examples/custom-types/uuid.mts create mode 100644 examples/custom-types/varint.mts create mode 100644 src/types/float.rs create mode 100644 src/types/varint.rs diff --git a/Cargo.toml b/Cargo.toml index 602e30c..c16ea9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,14 @@ crate-type = ["cdylib"] [dependencies] # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix -napi = { version = "2.13.3", default-features = false, features = [ +napi = { version = "2.16", default-features = false, features = [ "napi8", "async", "serde", "serde_json", "serde-json", ] } -napi-derive = "2.13.0" +napi-derive = "2.16" tokio = { version = "1", features = ["full"] } scylla = { version = "0.13.1", features = [ "ssl", diff --git a/examples/custom-types/floats.mts b/examples/custom-types/floats.mts new file mode 100644 index 0000000..9afbfa9 --- /dev/null +++ b/examples/custom-types/floats.mts @@ -0,0 +1,22 @@ +import { Cluster, Float } from "../../index.js"; + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; + +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ nodes }); +const session = await cluster.connect(); + +await session.execute( + "CREATE KEYSPACE IF NOT EXISTS floats WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", +); +await session.useKeyspace("floats"); + +await session.execute( + "CREATE TABLE IF NOT EXISTS floats (a float, primary key (a))", +); + +await session.execute("INSERT INTO floats (a) VALUES (?)", [new Float(1.1)]); // :( + +const results = await session.execute("SELECT a FROM floats"); +console.log(results); diff --git a/examples/custom-types/list.mts b/examples/custom-types/list.mts new file mode 100644 index 0000000..e69de29 diff --git a/examples/custom-types/set.mts b/examples/custom-types/set.mts new file mode 100644 index 0000000..e69de29 diff --git a/examples/custom-types/uuid.mts b/examples/custom-types/uuid.mts new file mode 100644 index 0000000..e69de29 diff --git a/examples/custom-types/varint.mts b/examples/custom-types/varint.mts new file mode 100644 index 0000000..960866d --- /dev/null +++ b/examples/custom-types/varint.mts @@ -0,0 +1,24 @@ +import { Cluster, Varint } from "../../index.js"; + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; + +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ nodes }); +const session = await cluster.connect(); + +await session.execute( + "CREATE KEYSPACE IF NOT EXISTS varints WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", +); +await session.useKeyspace("varints"); + +await session.execute( + "CREATE TABLE IF NOT EXISTS varints (a varint, primary key (a))", +); + +await session.execute("INSERT INTO varints (a) VALUES (?)", [ + new Varint([0x00, 0x01, 0x02]), +]); + +const results = await session.execute("SELECT a FROM varints"); +console.log(results); diff --git a/examples/refactor-test.mts b/examples/refactor-test.mts index b2d4a6b..96cc540 100644 --- a/examples/refactor-test.mts +++ b/examples/refactor-test.mts @@ -1,4 +1,4 @@ -import { Cluster, Uuid, Duration, Decimal } from "../index.js"; +import { Cluster, Uuid, Duration, Decimal, Float, Varint } from "../index.js"; const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; @@ -19,21 +19,27 @@ await session.execute( c duration, d decimal, e blob, + f float, + g varint, primary key (a) )`, ); -// TODO: varint, float, map +// TODO: varint, map -// const a = await session -// .execute("INSERT INTO refactor (a, b, c, d, e) VALUES (?, ?, ?, ?, ?)", [ -// 1, -// true, -// new Duration(1, 1, 1), -// new Decimal([0x01, 0xe2, 0x40], 3), -// Buffer.from("hello").toJSON().data, -// ]) -// .catch(console.error); -// console.log(a); +const a = await session + .execute( + "INSERT INTO refactor (a, b, c, d, e, f, g) VALUES (?, ?, ?, ?, ?, ?, ?)", + [ + 1, + true, + new Duration(1, 1, 1), + new Decimal([0x01, 0xe2, 0x40], 3), + Buffer.from("hello").toJSON().data, + new Float(8.3212), + ], + ) + .catch(console.error); +console.log(a); const results = await session.execute("SELECT * FROM refactor"); console.log(results); diff --git a/index.d.ts b/index.d.ts index a5d62a3..01cc947 100644 --- a/index.d.ts +++ b/index.d.ts @@ -173,8 +173,7 @@ export class ScyllaSession { * driver does not check it by itself, so incorrect data will be written if the order is * wrong. */ - execute(query: string | Query | PreparedStatement, parameters?: Array | Record>> | undefined | null, options?: QueryOptions | undefined | null): Promise - query(scyllaQuery: Query, parameters?: Array | Record>> | undefined | null): Promise + execute(query: string | Query | PreparedStatement, parameters?: Array | Float | Varint | Record | Float | Varint>> | undefined | null, options?: QueryOptions | undefined | null): Promise prepare(query: string): Promise /** * Perform a batch query\ @@ -212,9 +211,6 @@ export class ScyllaSession { * * console.log(await session.execute("SELECT * FROM users")); * ``` - */ - batch(batch: BatchStatement, parameters: Array | Record>> | undefined | null>): Promise - /** * Sends `USE ` request on all connections\ * This allows to write `SELECT * FROM table` instead of `SELECT * FROM keyspace.table`\ * @@ -303,6 +299,15 @@ export class Duration { /** Returns the string representation of the Duration. */ toString(): string } +/** + * A float number. + * + * Due to the nature of numbers in JavaScript, it's hard to distinguish between integers and floats, so this type is used to represent + * float numbers while any other JS number will be treated as an integer. (This is not the case for BigInts, which are always treated as BigInts). + */ +export class Float { + constructor(inner: number) +} export class Uuid { /** Generates a random UUID v4. */ static randomV4(): Uuid @@ -311,6 +316,26 @@ export class Uuid { /** Returns the string representation of the UUID. */ toString(): string } +/** + * Native CQL `varint` representation. + * + * Represented as two's-complement binary in big-endian order. + * + * This type is a raw representation in bytes. It's the default + * implementation of `varint` type - independent of any + * external crates and crate features. + * + * # DB data format + * Notice that constructors don't perform any normalization + * on the provided data. This means that underlying bytes may + * contain leading zeros. + * + * Currently, Scylla and Cassandra support non-normalized `varint` values. + * Bytes provided by the user via constructor are passed to DB as is. + */ +export class Varint { + constructor(inner: Array) +} type NativeTypes = number | string | Uuid | bigint | Duration | Decimal; type WithMapType = NativeTypes | Record; diff --git a/index.js b/index.js index 6822c84..1786ad1 100644 --- a/index.js +++ b/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Uuid } = nativeBinding +const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Float, Uuid, Varint } = nativeBinding module.exports.Compression = Compression module.exports.Consistency = Consistency @@ -325,7 +325,9 @@ module.exports.ScyllaSession = ScyllaSession module.exports.ScyllaClusterData = ScyllaClusterData module.exports.Decimal = Decimal module.exports.Duration = Duration +module.exports.Float = Float module.exports.Uuid = Uuid +module.exports.Varint = Varint const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') diff --git a/src/helpers/query_parameter.rs b/src/helpers/query_parameter.rs index 1a62fab..25a0abb 100644 --- a/src/helpers/query_parameter.rs +++ b/src/helpers/query_parameter.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; -use crate::types::{decimal::Decimal, duration::Duration, uuid::Uuid}; -use napi::bindgen_prelude::{BigInt, Either8, Either9}; +use crate::types::{ + decimal::Decimal, duration::Duration, float::Float, uuid::Uuid, varint::Varint, +}; +use napi::bindgen_prelude::{BigInt, Either10, Either11}; use scylla::{ frame::response::result::CqlValue, serialize::{ @@ -13,14 +15,15 @@ use scylla::{ macro_rules! define_expected_type { ($lifetime:lifetime, $($t:ty),+) => { - pub type ParameterNativeTypes<$lifetime> = Either8<$($t),+>; - pub type ParameterWithMapType<$lifetime> = Either9<$($t),+, HashMap>>; + pub type ParameterNativeTypes<$lifetime> = Either10<$($t),+>; + pub type ParameterWithMapType<$lifetime> = Either11<$($t),+, HashMap>>; pub type JSQueryParameters<$lifetime> = napi::Result>>>; }; } -define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal, bool, Vec); +define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal, bool, Vec, &'a Float, &'a Varint); +#[derive(Debug, Clone)] pub struct QueryParameter<'a> { #[allow(clippy::type_complexity)] pub(crate) parameters: Option>>, @@ -66,7 +69,15 @@ impl<'a> SerializeRow for QueryParameter<'a> { CqlValue::Blob(u32_vec_to_u8_vec(buffer)) .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } - ParameterWithMapType::I(map) => { + ParameterWithMapType::I(float) => { + CqlValue::Float((*float).into()) + .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; + } + ParameterWithMapType::J(varint) => { + CqlValue::Varint((*varint).into()) + .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; + } + ParameterWithMapType::K(map) => { CqlValue::UserDefinedType { // FIXME: I'm not sure why this is even necessary tho, but if it's and makes sense we'll have to make it so we get the correct info keyspace: "keyspace".to_string(), @@ -100,6 +111,12 @@ impl<'a> SerializeRow for QueryParameter<'a> { key.to_string(), Some(CqlValue::Blob(u32_vec_to_u8_vec(buffer))), ), + ParameterNativeTypes::J(varint) => { + (key.to_string(), Some(CqlValue::Varint((*varint).into()))) + } + ParameterNativeTypes::I(float) => { + (key.to_string(), Some(CqlValue::Float((*float).into()))) + } }) .collect::)>>(), } diff --git a/src/helpers/query_results.rs b/src/helpers/query_results.rs index a3a63c7..4d5febe 100644 --- a/src/helpers/query_results.rs +++ b/src/helpers/query_results.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use napi::bindgen_prelude::{BigInt, Either10, Either9}; +use napi::bindgen_prelude::{BigInt, Either9, Either10}; use scylla::frame::response::result::{ColumnType, CqlValue}; use crate::types::{decimal::Decimal, duration::Duration, uuid::Uuid}; diff --git a/src/session/scylla_session.rs b/src/session/scylla_session.rs index e5b00ff..2bcfb4b 100644 --- a/src/session/scylla_session.rs +++ b/src/session/scylla_session.rs @@ -5,9 +5,11 @@ use crate::query::scylla_prepared_statement::PreparedStatement; use crate::query::scylla_query::Query; use crate::types::decimal::Decimal; use crate::types::duration::Duration; +use crate::types::float::Float; use crate::types::uuid::Uuid; +use crate::types::varint::Varint; use napi::Either; -use napi::bindgen_prelude::{BigInt, Either3, Either8, Either9}; +use napi::bindgen_prelude::{BigInt, Either3, Either10, Either11}; use std::collections::HashMap; use super::metrics; @@ -67,7 +69,7 @@ impl ScyllaSession { query: Either3, parameters: Option< Vec< - Either9< + Either11< u32, String, &Uuid, @@ -76,7 +78,23 @@ impl ScyllaSession { &Decimal, bool, Vec, - HashMap>>, + &Float, + &Varint, + HashMap< + String, + Either10< + u32, + String, + &Uuid, + BigInt, + &Duration, + &Decimal, + bool, + Vec, + &Float, + &Varint, + >, + >, >, >, >, @@ -171,46 +189,50 @@ impl ScyllaSession { QueryResult::parser(query_result) } - #[allow(clippy::type_complexity)] - #[napi] - pub async fn query( - &self, - scylla_query: &Query, - parameters: Option< - Vec< - Either9< - u32, - String, - &Uuid, - BigInt, - &Duration, - &Decimal, - bool, - Vec, - HashMap>>, - >, - >, - >, - ) -> JSQueryResult { - let values = QueryParameter::parser(parameters.clone()).ok_or(napi::Error::new( - napi::Status::InvalidArg, - format!("Something went wrong with your query parameters. {parameters:?}"), - ))?; + // #[allow(clippy::type_complexity)] + // #[napi] + // pub async fn query( + // &self, + // scylla_query: &Query, + // parameters: Option< + // Vec< + // Either10< + // u32, + // String, + // &Uuid, + // BigInt, + // &Duration, + // &Decimal, + // bool, + // Vec, + // &Float, + // HashMap< + // String, + // Either9, &Float>, + // >, + // >, + // >, + // >, + // ) -> JSQueryResult { + // let values = QueryParameter::parser(parameters.clone()).ok_or(napi::Error::new( + // napi::Status::InvalidArg, + // format!("Something went wrong with your query parameters. {parameters:?}"), + // ))?; - let query_result = self - .session - .query(scylla_query.query.clone(), values) - .await - .map_err(|e| { - napi::Error::new( - napi::Status::InvalidArg, - // format!("Something went wrong with your query. - [{scylla_query}] - {parameters:?}\n{e}"), - format!("Something went wrong with your query. - [{scylla_query}] - TMP\n{e}"), - ) - })?; + // let query_result = self + // .session + // .query(scylla_query.query.clone(), values) + // .await + // .map_err(|e| { + // napi::Error::new( + // napi::Status::InvalidArg, + // // format!("Something went wrong with your query. - [{scylla_query}] - {parameters:?}\n{e}"), + // format!("Something went wrong with your query. - [{scylla_query}] - TMP\n{e}"), + // ) + // })?; - QueryResult::parser(query_result) - } + // QueryResult::parser(query_result) + // } #[napi] pub async fn prepare(&self, query: String) -> napi::Result { @@ -259,55 +281,56 @@ impl ScyllaSession { /// /// console.log(await session.execute("SELECT * FROM users")); /// ``` - #[napi] - #[allow(clippy::type_complexity)] - pub async fn batch( - &self, - batch: &ScyllaBatchStatement, - parameters: Vec< - Option< - Vec< - Either9< - u32, - String, - &Uuid, - BigInt, - &Duration, - &Decimal, - bool, - Vec, - HashMap< - String, - Either8>, - >, - >, - >, - >, - >, - ) -> JSQueryResult { - let values = parameters - .iter() - .map(|params| { - QueryParameter::parser(params.clone()).ok_or(napi::Error::new( - napi::Status::InvalidArg, - format!("Something went wrong with your batch parameters. {parameters:?}"), - )) - }) - .collect::>>()?; + // #[napi] + // #[allow(clippy::type_complexity)] + // pub async fn batch( + // &self, + // batch: &ScyllaBatchStatement, + // parameters: Vec< + // Option< + // Vec< + // Either10< + // u32, + // String, + // &Uuid, + // BigInt, + // &Duration, + // &Decimal, + // bool, + // Vec, + // &Float, + // HashMap< + // String, + // Either9, &Float>, + // >, + // >, + // >, + // >, + // >, + // ) -> JSQueryResult { + // let values = parameters + // .iter() + // .map(|params| { + // QueryParameter::parser(params.clone()).ok_or(napi::Error::new( + // napi::Status::InvalidArg, + // format!("Something went wrong with your batch parameters. {parameters:?}"), + // )) + // }) + // .collect::>>()?; - let query_result = self - .session - .batch(&batch.batch, values) - .await - .map_err(|e| { - napi::Error::new( - napi::Status::InvalidArg, - format!("Something went wrong with your batch. - [{batch}] - {parameters:?}\n{e}"), - ) - })?; + // let query_result = self + // .session + // .batch(&batch.batch, values) + // .await + // .map_err(|e| { + // napi::Error::new( + // napi::Status::InvalidArg, + // format!("Something went wrong with your batch. - [{batch}] - {parameters:?}\n{e}"), + // ) + // })?; - QueryResult::parser(query_result) - } + // QueryResult::parser(query_result) + // } /// Sends `USE ` request on all connections\ /// This allows to write `SELECT * FROM table` instead of `SELECT * FROM keyspace.table`\ diff --git a/src/types/float.rs b/src/types/float.rs new file mode 100644 index 0000000..b829cf4 --- /dev/null +++ b/src/types/float.rs @@ -0,0 +1,35 @@ +/// A float number. +/// +/// Due to the nature of numbers in JavaScript, it's hard to distinguish between integers and floats, so this type is used to represent +/// float numbers while any other JS number will be treated as an integer. (This is not the case for BigInts, which are always treated as BigInts). +#[napi] +#[derive(Debug, Copy, Clone)] +pub struct Float { + pub(crate) inner: f64, +} + +impl From for Float { + fn from(inner: f64) -> Self { + Self { inner } + } +} + +impl From for f64 { + fn from(float: Float) -> Self { + float.inner + } +} + +impl From<&Float> for f32 { + fn from(float: &Float) -> Self { + float.inner as f32 + } +} + +#[napi] +impl Float { + #[napi(constructor)] + pub fn new_float(inner: f64) -> Float { + Float::from(inner) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 8f2b505..91bba3f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,3 +1,5 @@ pub mod decimal; pub mod duration; +pub mod float; pub mod uuid; +pub mod varint; diff --git a/src/types/varint.rs b/src/types/varint.rs new file mode 100644 index 0000000..a6983ad --- /dev/null +++ b/src/types/varint.rs @@ -0,0 +1,48 @@ +use scylla::frame::value::CqlVarint; + +/// Native CQL `varint` representation. +/// +/// Represented as two's-complement binary in big-endian order. +/// +/// This type is a raw representation in bytes. It's the default +/// implementation of `varint` type - independent of any +/// external crates and crate features. +/// +/// # DB data format +/// Notice that constructors don't perform any normalization +/// on the provided data. This means that underlying bytes may +/// contain leading zeros. +/// +/// Currently, Scylla and Cassandra support non-normalized `varint` values. +/// Bytes provided by the user via constructor are passed to DB as is. +#[napi] +#[derive(Debug, Clone)] +pub struct Varint { + pub(crate) inner: Vec, +} + +impl From> for Varint { + fn from(inner: Vec) -> Self { + Self { inner } + } +} + +impl From for Vec { + fn from(varint: Varint) -> Self { + varint.inner + } +} + +impl From<&Varint> for CqlVarint { + fn from(varint: &Varint) -> Self { + CqlVarint::from_signed_bytes_be(varint.inner.clone()) + } +} + +#[napi] +impl Varint { + #[napi(constructor)] + pub fn new_varint(inner: Vec) -> Varint { + Varint::from(inner) + } +} From 095c9032d0af6857da1f0ff21e2d15a4f8347caf Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Sat, 2 Nov 2024 17:06:30 -0300 Subject: [PATCH 06/13] feat(types): add support for the type `list` Signed-off-by: Daniel Boll --- examples/custom-types/bigint.mts | 22 ++++ examples/custom-types/floats.mts | 2 +- examples/custom-types/list.mts | 26 ++++ examples/custom-types/map.mts | 28 +++++ examples/custom-types/set.mts | 24 ++++ examples/{ => custom-types}/udt.mts | 2 +- examples/custom-types/uuid.mts | 22 ++++ index.d.ts | 15 ++- index.js | 3 +- scripts/fix-files.mjs | 5 +- src/helpers/cql_value_bridge.rs | 65 ++++++++++ src/helpers/mod.rs | 2 + src/helpers/query_parameter.rs | 120 ++---------------- src/helpers/query_results.rs | 50 ++++++-- src/helpers/to_cql_value.rs | 83 +++++++++++++ src/session/scylla_session.rs | 182 +++++++++------------------- src/types/list.rs | 37 ++++++ src/types/mod.rs | 1 + 18 files changed, 432 insertions(+), 257 deletions(-) create mode 100644 examples/custom-types/bigint.mts create mode 100644 examples/custom-types/map.mts rename examples/{ => custom-types}/udt.mts (95%) create mode 100644 src/helpers/cql_value_bridge.rs create mode 100644 src/helpers/to_cql_value.rs create mode 100644 src/types/list.rs diff --git a/examples/custom-types/bigint.mts b/examples/custom-types/bigint.mts new file mode 100644 index 0000000..ddf6ffa --- /dev/null +++ b/examples/custom-types/bigint.mts @@ -0,0 +1,22 @@ +import { Cluster } from "../../index.js"; + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; + +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ nodes }); +const session = await cluster.connect(); + +await session.execute( + "CREATE KEYSPACE IF NOT EXISTS bigints WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", +); +await session.useKeyspace("bigints"); + +await session.execute( + "CREATE TABLE IF NOT EXISTS bigints (a bigint, primary key (a))", +); + +await session.execute("INSERT INTO bigints (a) VALUES (?)", [1238773128n]); + +const results = await session.execute("SELECT a FROM bigints"); +console.log(results); diff --git a/examples/custom-types/floats.mts b/examples/custom-types/floats.mts index 9afbfa9..adbf83c 100644 --- a/examples/custom-types/floats.mts +++ b/examples/custom-types/floats.mts @@ -16,7 +16,7 @@ await session.execute( "CREATE TABLE IF NOT EXISTS floats (a float, primary key (a))", ); -await session.execute("INSERT INTO floats (a) VALUES (?)", [new Float(1.1)]); // :( +await session.execute("INSERT INTO floats (a) VALUES (?)", [new Float(1.1)]); const results = await session.execute("SELECT a FROM floats"); console.log(results); diff --git a/examples/custom-types/list.mts b/examples/custom-types/list.mts index e69de29..8cd9606 100644 --- a/examples/custom-types/list.mts +++ b/examples/custom-types/list.mts @@ -0,0 +1,26 @@ +import { Cluster, List, Uuid } from "../../index.js"; + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; + +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ nodes }); +const session = await cluster.connect(); + +await session.execute( + "CREATE KEYSPACE IF NOT EXISTS lists WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", +); +await session.useKeyspace("lists"); + +await session.execute( + "CREATE TABLE IF NOT EXISTS lists (a uuid, b list, primary key (a))", +); + +// NOTE: driver is not throwing errors if the return of the function is not used. +await session.execute("INSERT INTO lists (a, b) VALUES (?, ?)", [ + Uuid.randomV4(), + new List([1, 2, 3]), +]); + +const results = await session.execute("SELECT * FROM lists"); +console.log(results); diff --git a/examples/custom-types/map.mts b/examples/custom-types/map.mts new file mode 100644 index 0000000..44dbb4f --- /dev/null +++ b/examples/custom-types/map.mts @@ -0,0 +1,28 @@ +import { Cluster, Map } from "../../index.js"; + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; + +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ nodes }); +const session = await cluster.connect(); + +await session.execute( + "CREATE KEYSPACE IF NOT EXISTS maps WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", +); +await session.useKeyspace("maps"); + +await session.execute( + "CREATE TABLE IF NOT EXISTS maps (a map, primary key (a))", +); + +await session.execute("INSERT INTO maps (a) VALUES (?)", [ + new Map([ + ["a", 1], + ["b", 2], + ["c", 3], + ]), +]); + +const results = await session.execute("SELECT a FROM maps"); +console.log(results); diff --git a/examples/custom-types/set.mts b/examples/custom-types/set.mts index e69de29..61b3450 100644 --- a/examples/custom-types/set.mts +++ b/examples/custom-types/set.mts @@ -0,0 +1,24 @@ +import { Cluster, Set } from "../../index.js"; + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; + +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ nodes }); +const session = await cluster.connect(); + +await session.execute( + "CREATE KEYSPACE IF NOT EXISTS sets WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", +); +await session.useKeyspace("sets"); + +await session.execute( + "CREATE TABLE IF NOT EXISTS sets (a set, primary key (a))", +); + +await session.execute("INSERT INTO sets (a) VALUES (?)", [ + new Set([1, 2, 3]), +]); + +const results = await session.execute("SELECT a FROM sets"); +console.log(results); diff --git a/examples/udt.mts b/examples/custom-types/udt.mts similarity index 95% rename from examples/udt.mts rename to examples/custom-types/udt.mts index 3cefc1d..4d4af51 100644 --- a/examples/udt.mts +++ b/examples/custom-types/udt.mts @@ -1,4 +1,4 @@ -import { Cluster } from "../index.js"; +import { Cluster } from "../../index.js"; const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; diff --git a/examples/custom-types/uuid.mts b/examples/custom-types/uuid.mts index e69de29..c4d4ab4 100644 --- a/examples/custom-types/uuid.mts +++ b/examples/custom-types/uuid.mts @@ -0,0 +1,22 @@ +import { Cluster, Uuid } from "../../index.js"; + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; + +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ nodes }); +const session = await cluster.connect(); + +await session.execute( + "CREATE KEYSPACE IF NOT EXISTS uuids WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", +); +await session.useKeyspace("uuids"); + +await session.execute( + "CREATE TABLE IF NOT EXISTS uuids (a uuid, primary key (a))", +); + +await session.execute("INSERT INTO uuids (a) VALUES (?)", [Uuid.randomV4()]); + +const results = await session.execute("SELECT a FROM uuids"); +console.log(results); diff --git a/index.d.ts b/index.d.ts index 01cc947..bb841b1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -173,7 +173,8 @@ export class ScyllaSession { * driver does not check it by itself, so incorrect data will be written if the order is * wrong. */ - execute(query: string | Query | PreparedStatement, parameters?: Array | Float | Varint | Record | Float | Varint>> | undefined | null, options?: QueryOptions | undefined | null): Promise + execute(query: string | Query | PreparedStatement, parameters?: Array | undefined | null, options?: QueryOptions | undefined | null): Promise + query(scyllaQuery: Query, parameters?: Array | undefined | null): Promise prepare(query: string): Promise /** * Perform a batch query\ @@ -211,6 +212,9 @@ export class ScyllaSession { * * console.log(await session.execute("SELECT * FROM users")); * ``` + */ + batch(batch: BatchStatement, parameters: Array | undefined | null>): Promise + /** * Sends `USE ` request on all connections\ * This allows to write `SELECT * FROM table` instead of `SELECT * FROM keyspace.table`\ * @@ -308,6 +312,10 @@ export class Duration { export class Float { constructor(inner: number) } +/** A list of any CqlType */ +export class List { + constructor(values: Array) +} export class Uuid { /** Generates a random UUID v4. */ static randomV4(): Uuid @@ -337,6 +345,7 @@ export class Varint { constructor(inner: Array) } -type NativeTypes = number | string | Uuid | bigint | Duration | Decimal; -type WithMapType = NativeTypes | Record; +type NativeTypes = number | string | Uuid | bigint | Duration | Decimal | Float | List; +type WithMapType = NativeTypes | Record | NativeTypes[]; +type ParameterWithMapType = WithMapType; type JSQueryResultType = Record[]; \ No newline at end of file diff --git a/index.js b/index.js index 1786ad1..931fa30 100644 --- a/index.js +++ b/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Float, Uuid, Varint } = nativeBinding +const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Float, List, Uuid, Varint } = nativeBinding module.exports.Compression = Compression module.exports.Consistency = Consistency @@ -326,6 +326,7 @@ module.exports.ScyllaClusterData = ScyllaClusterData module.exports.Decimal = Decimal module.exports.Duration = Duration module.exports.Float = Float +module.exports.List = List module.exports.Uuid = Uuid module.exports.Varint = Varint diff --git a/scripts/fix-files.mjs b/scripts/fix-files.mjs index 1a4b995..6f3ddbe 100644 --- a/scripts/fix-files.mjs +++ b/scripts/fix-files.mjs @@ -24,8 +24,9 @@ function addJSQueryResultType(filename) { readFileSync(filename, "utf8") .concat( ` -type NativeTypes = number | string | Uuid | bigint | Duration | Decimal; -type WithMapType = NativeTypes | Record; +type NativeTypes = number | string | Uuid | bigint | Duration | Decimal | Float | List; +type WithMapType = NativeTypes | Record | NativeTypes[]; +type ParameterWithMapType = WithMapType; type JSQueryResultType = Record[]; `, ) diff --git a/src/helpers/cql_value_bridge.rs b/src/helpers/cql_value_bridge.rs new file mode 100644 index 0000000..443aa8c --- /dev/null +++ b/src/helpers/cql_value_bridge.rs @@ -0,0 +1,65 @@ +use napi::bindgen_prelude::{BigInt, Either11, Either12}; +use scylla::frame::response::result::CqlValue; + +use std::collections::HashMap; + +use crate::types::{ + decimal::Decimal, duration::Duration, float::Float, list::List, uuid::Uuid, varint::Varint, +}; + +use super::to_cql_value::ToCqlValue; + +macro_rules! define_expected_type { + ($lifetime:lifetime, $($t:ty),+) => { + pub type ParameterNativeTypes<$lifetime> = Either11<$($t),+>; + pub type ParameterWithMapType<$lifetime> = Either12<$($t),+, HashMap>>; + pub type JSQueryParameters<$lifetime> = napi::Result>>>; + }; +} + +define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal, bool, Vec, &'a Float, &'a Varint, &'a List); + +impl<'a> ToCqlValue for ParameterWithMapType<'a> { + fn to_cql_value(&self) -> CqlValue { + match self { + ParameterWithMapType::A(num) => num.to_cql_value(), + ParameterWithMapType::B(str) => str.to_cql_value(), + ParameterWithMapType::C(uuid) => uuid.to_cql_value(), + ParameterWithMapType::D(bigint) => bigint.to_cql_value(), + ParameterWithMapType::E(duration) => duration.to_cql_value(), + ParameterWithMapType::F(decimal) => decimal.to_cql_value(), + ParameterWithMapType::G(bool_val) => bool_val.to_cql_value(), + ParameterWithMapType::H(buffer) => buffer.to_cql_value(), + ParameterWithMapType::I(float) => float.to_cql_value(), + ParameterWithMapType::J(varint) => varint.to_cql_value(), + ParameterWithMapType::K(list) => list.to_cql_value(), + ParameterWithMapType::L(map) => CqlValue::UserDefinedType { + // TODO: think a better way to fill this info here + keyspace: "keyspace".to_string(), + type_name: "type_name".to_string(), + fields: map + .iter() + .map(|(key, value)| (key.clone(), Some(value.to_cql_value()))) + .collect::)>>(), + }, + } + } +} + +impl<'a> ToCqlValue for ParameterNativeTypes<'a> { + fn to_cql_value(&self) -> CqlValue { + match self { + ParameterNativeTypes::A(num) => num.to_cql_value(), + ParameterNativeTypes::B(str) => str.to_cql_value(), + ParameterNativeTypes::C(uuid) => uuid.to_cql_value(), + ParameterNativeTypes::D(bigint) => bigint.to_cql_value(), + ParameterNativeTypes::E(duration) => duration.to_cql_value(), + ParameterNativeTypes::F(decimal) => decimal.to_cql_value(), + ParameterNativeTypes::G(bool_val) => bool_val.to_cql_value(), + ParameterNativeTypes::H(buffer) => buffer.to_cql_value(), + ParameterNativeTypes::J(varint) => varint.to_cql_value(), + ParameterNativeTypes::I(float) => float.to_cql_value(), + ParameterNativeTypes::K(list) => list.to_cql_value(), + } + } +} diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs index f341ed0..9216d25 100644 --- a/src/helpers/mod.rs +++ b/src/helpers/mod.rs @@ -1,2 +1,4 @@ +pub mod cql_value_bridge; pub mod query_parameter; pub mod query_results; +pub mod to_cql_value; diff --git a/src/helpers/query_parameter.rs b/src/helpers/query_parameter.rs index 25a0abb..1a3af00 100644 --- a/src/helpers/query_parameter.rs +++ b/src/helpers/query_parameter.rs @@ -1,27 +1,10 @@ -use std::collections::HashMap; - -use crate::types::{ - decimal::Decimal, duration::Duration, float::Float, uuid::Uuid, varint::Varint, -}; -use napi::bindgen_prelude::{BigInt, Either10, Either11}; -use scylla::{ - frame::response::result::CqlValue, - serialize::{ - RowWriter, SerializationError, - row::{RowSerializationContext, SerializeRow}, - value::SerializeCql, - }, +use scylla::serialize::{ + RowWriter, SerializationError, + row::{RowSerializationContext, SerializeRow}, + value::SerializeCql, }; -macro_rules! define_expected_type { - ($lifetime:lifetime, $($t:ty),+) => { - pub type ParameterNativeTypes<$lifetime> = Either10<$($t),+>; - pub type ParameterWithMapType<$lifetime> = Either11<$($t),+, HashMap>>; - pub type JSQueryParameters<$lifetime> = napi::Result>>>; - }; -} - -define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal, bool, Vec, &'a Float, &'a Varint); +use super::{cql_value_bridge::ParameterWithMapType, to_cql_value::ToCqlValue}; #[derive(Debug, Clone)] pub struct QueryParameter<'a> { @@ -37,92 +20,9 @@ impl<'a> SerializeRow for QueryParameter<'a> { ) -> Result<(), SerializationError> { if let Some(parameters) = &self.parameters { for (i, parameter) in parameters.iter().enumerate() { - match parameter { - ParameterWithMapType::A(num) => { - CqlValue::Int(*num as i32) - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::B(str) => { - CqlValue::Text(str.to_string()) - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::C(uuid) => { - CqlValue::Uuid(uuid.get_inner()) - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::D(bigint) => { - CqlValue::BigInt(bigint.get_i64().0) - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::E(duration) => { - CqlValue::Duration((**duration).into()) - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::F(decimal) => { - CqlValue::Decimal((*decimal).into()) - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::G(bool) => { - CqlValue::Boolean(*bool).serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::H(buffer) => { - CqlValue::Blob(u32_vec_to_u8_vec(buffer)) - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::I(float) => { - CqlValue::Float((*float).into()) - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::J(varint) => { - CqlValue::Varint((*varint).into()) - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - ParameterWithMapType::K(map) => { - CqlValue::UserDefinedType { - // FIXME: I'm not sure why this is even necessary tho, but if it's and makes sense we'll have to make it so we get the correct info - keyspace: "keyspace".to_string(), - type_name: "type_name".to_string(), - fields: map - .iter() - .map(|(key, value)| match value { - ParameterNativeTypes::A(num) => { - (key.to_string(), Some(CqlValue::Int(*num as i32))) - } - ParameterNativeTypes::B(str) => { - (key.to_string(), Some(CqlValue::Text(str.to_string()))) - } - ParameterNativeTypes::C(uuid) => { - (key.to_string(), Some(CqlValue::Uuid(uuid.get_inner()))) - } - ParameterNativeTypes::D(bigint) => { - (key.to_string(), Some(CqlValue::BigInt(bigint.get_i64().0))) - } - ParameterNativeTypes::E(duration) => ( - key.to_string(), - Some(CqlValue::Duration((**duration).into())), - ), - ParameterNativeTypes::F(decimal) => { - (key.to_string(), Some(CqlValue::Decimal((*decimal).into()))) - } - ParameterNativeTypes::G(bool) => { - (key.to_string(), Some(CqlValue::Boolean(*bool))) - } - ParameterNativeTypes::H(buffer) => ( - key.to_string(), - Some(CqlValue::Blob(u32_vec_to_u8_vec(buffer))), - ), - ParameterNativeTypes::J(varint) => { - (key.to_string(), Some(CqlValue::Varint((*varint).into()))) - } - ParameterNativeTypes::I(float) => { - (key.to_string(), Some(CqlValue::Float((*float).into()))) - } - }) - .collect::)>>(), - } - .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; - } - } + parameter + .to_cql_value() + .serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?; } } Ok(()) @@ -152,7 +52,3 @@ impl<'a> QueryParameter<'a> { }) } } - -fn u32_vec_to_u8_vec(input: &[u32]) -> Vec { - input.iter().map(|&num| num as u8).collect() -} diff --git a/src/helpers/query_results.rs b/src/helpers/query_results.rs index 4d5febe..548039f 100644 --- a/src/helpers/query_results.rs +++ b/src/helpers/query_results.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use napi::bindgen_prelude::{BigInt, Either9, Either10}; +use napi::bindgen_prelude::{BigInt, Either9, Either10, Either11}; use scylla::frame::response::result::{ColumnType, CqlValue}; use crate::types::{decimal::Decimal, duration::Duration, uuid::Uuid}; @@ -10,8 +10,9 @@ pub struct QueryResult { macro_rules! define_return_type { ($($t:ty),+) => { - type NativeTypes = Either9<$($t),+>; - type WithMapType = Either10<$($t),+, HashMap>; + type BaseTypes = Either9<$($t),+>; + type NativeTypes = Either10<$($t),+, Vec>; + type WithMapType = Either11<$($t),+, Vec, HashMap>; type ReturnType = napi::Result>; pub type JSQueryResult = napi::Result>>; }; @@ -115,18 +116,23 @@ impl QueryResult { }) .collect::>>>(); - Ok(WithMapType::J(map?.unwrap())) + Ok(WithMapType::K(map?.unwrap())) } - ColumnType::UserDefinedType { field_types, .. } => Ok(WithMapType::J(Self::parse_udt( + ColumnType::UserDefinedType { field_types, .. } => Ok(WithMapType::K(Self::parse_udt( column.as_udt().unwrap(), field_types, )?)), ColumnType::Custom(_) => Ok(WithMapType::A( "ColumnType Custom not supported yet".to_string(), )), - ColumnType::List(_) => Ok(WithMapType::A( - "ColumnType List not supported yet".to_string(), - )), + ColumnType::List(list_type) => Ok(WithMapType::J(Self::extract_base_types( + column + .as_list() + .unwrap() + .iter() + .map(|e| Self::parse_value(&Some(e.clone()), list_type)) + .collect::>(), + )?)), ColumnType::Set(_) => Ok(WithMapType::A( "ColumnType Set not supported yet".to_string(), )), @@ -165,11 +171,37 @@ impl QueryResult { WithMapType::G(a) => Ok(NativeTypes::G(a)), WithMapType::H(a) => Ok(NativeTypes::H(a)), WithMapType::I(a) => Ok(NativeTypes::I(a)), - WithMapType::J(_) => Err(napi::Error::new( + WithMapType::J(a) => Ok(NativeTypes::J(a)), + WithMapType::K(_) => Err(napi::Error::new( napi::Status::GenericFailure, "Map type is not supported in this context".to_string(), )), }) .transpose() } + + fn extract_base_types(return_types: Vec) -> napi::Result> { + return_types + .into_iter() + .filter_map(|return_type| { + return_type.ok().and_then(|opt_with_map_type| { + opt_with_map_type.map(|with_map_type| match with_map_type { + WithMapType::A(a) => Ok(BaseTypes::A(a)), + WithMapType::B(b) => Ok(BaseTypes::B(b)), + WithMapType::C(c) => Ok(BaseTypes::C(c)), + WithMapType::D(d) => Ok(BaseTypes::D(d)), + WithMapType::E(e) => Ok(BaseTypes::E(e)), + WithMapType::F(f) => Ok(BaseTypes::F(f)), + WithMapType::G(g) => Ok(BaseTypes::G(g)), + WithMapType::H(h) => Ok(BaseTypes::H(h)), + WithMapType::I(i) => Ok(BaseTypes::I(i)), + WithMapType::J(_) | WithMapType::K(_) => Err(napi::Error::new( + napi::Status::GenericFailure, + "Nested collections or maps are not supported".to_string(), + )), + }) + }) + }) + .collect::>>() + } } diff --git a/src/helpers/to_cql_value.rs b/src/helpers/to_cql_value.rs new file mode 100644 index 0000000..6ef1a58 --- /dev/null +++ b/src/helpers/to_cql_value.rs @@ -0,0 +1,83 @@ +use napi::bindgen_prelude::BigInt; +use scylla::frame::response::result::CqlValue; + +use crate::types::{ + decimal::Decimal, duration::Duration, float::Float, list::List, uuid::Uuid, varint::Varint, +}; + +// Trait to abstract the conversion to CqlValue +pub trait ToCqlValue { + fn to_cql_value(&self) -> CqlValue; +} + +// Implement ToCqlValue for various types +impl ToCqlValue for u32 { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Int(*self as i32) + } +} + +impl ToCqlValue for String { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Text(self.clone()) + } +} + +impl ToCqlValue for &Uuid { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Uuid(self.get_inner()) + } +} + +impl ToCqlValue for BigInt { + fn to_cql_value(&self) -> CqlValue { + CqlValue::BigInt(self.get_i64().0) + } +} + +impl ToCqlValue for &Duration { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Duration((**self).into()) + } +} + +impl ToCqlValue for &Decimal { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Decimal((*self).into()) + } +} + +impl ToCqlValue for bool { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Boolean(*self) + } +} + +impl ToCqlValue for &Float { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Float((*self).into()) + } +} + +impl ToCqlValue for &Varint { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Varint((*self).into()) + } +} + +impl ToCqlValue for &List { + fn to_cql_value(&self) -> CqlValue { + CqlValue::List(self.inner.clone()) + } +} + +// Helper function to convert u32 vector to u8 vector +fn u32_vec_to_u8_vec(input: &[u32]) -> Vec { + input.iter().map(|&num| num as u8).collect() +} + +impl ToCqlValue for Vec { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Blob(u32_vec_to_u8_vec(self)) + } +} diff --git a/src/session/scylla_session.rs b/src/session/scylla_session.rs index 2bcfb4b..7756810 100644 --- a/src/session/scylla_session.rs +++ b/src/session/scylla_session.rs @@ -1,16 +1,12 @@ +use crate::helpers::cql_value_bridge::ParameterWithMapType; use crate::helpers::query_parameter::QueryParameter; use crate::helpers::query_results::{JSQueryResult, QueryResult}; use crate::query::batch_statement::ScyllaBatchStatement; use crate::query::scylla_prepared_statement::PreparedStatement; use crate::query::scylla_query::Query; -use crate::types::decimal::Decimal; -use crate::types::duration::Duration; -use crate::types::float::Float; use crate::types::uuid::Uuid; -use crate::types::varint::Varint; use napi::Either; -use napi::bindgen_prelude::{BigInt, Either3, Either10, Either11}; -use std::collections::HashMap; +use napi::bindgen_prelude::Either3; use super::metrics; use super::topology::ScyllaClusterData; @@ -62,42 +58,11 @@ impl ScyllaSession { /// Order of fields in the object must match the order of fields as defined in the UDT. The /// driver does not check it by itself, so incorrect data will be written if the order is /// wrong. - #[allow(clippy::type_complexity)] #[napi] pub async fn execute( &self, query: Either3, - parameters: Option< - Vec< - Either11< - u32, - String, - &Uuid, - BigInt, - &Duration, - &Decimal, - bool, - Vec, - &Float, - &Varint, - HashMap< - String, - Either10< - u32, - String, - &Uuid, - BigInt, - &Duration, - &Decimal, - bool, - Vec, - &Float, - &Varint, - >, - >, - >, - >, - >, + parameters: Option>>, options: Option, ) -> JSQueryResult { let values = QueryParameter::parser(parameters.clone()).ok_or_else(|| { @@ -189,50 +154,31 @@ impl ScyllaSession { QueryResult::parser(query_result) } - // #[allow(clippy::type_complexity)] - // #[napi] - // pub async fn query( - // &self, - // scylla_query: &Query, - // parameters: Option< - // Vec< - // Either10< - // u32, - // String, - // &Uuid, - // BigInt, - // &Duration, - // &Decimal, - // bool, - // Vec, - // &Float, - // HashMap< - // String, - // Either9, &Float>, - // >, - // >, - // >, - // >, - // ) -> JSQueryResult { - // let values = QueryParameter::parser(parameters.clone()).ok_or(napi::Error::new( - // napi::Status::InvalidArg, - // format!("Something went wrong with your query parameters. {parameters:?}"), - // ))?; + #[allow(clippy::type_complexity)] + #[napi] + pub async fn query( + &self, + scylla_query: &Query, + parameters: Option>>, + ) -> JSQueryResult { + let values = QueryParameter::parser(parameters.clone()).ok_or(napi::Error::new( + napi::Status::InvalidArg, + format!("Something went wrong with your query parameters. {parameters:?}"), + ))?; - // let query_result = self - // .session - // .query(scylla_query.query.clone(), values) - // .await - // .map_err(|e| { - // napi::Error::new( - // napi::Status::InvalidArg, - // // format!("Something went wrong with your query. - [{scylla_query}] - {parameters:?}\n{e}"), - // format!("Something went wrong with your query. - [{scylla_query}] - TMP\n{e}"), - // ) - // })?; + let query_result = self + .session + .query(scylla_query.query.clone(), values) + .await + .map_err(|e| { + napi::Error::new( + napi::Status::InvalidArg, + format!("Something went wrong with your query. - [{scylla_query}] - {parameters:?}\n{e}"), + ) + })?; - // QueryResult::parser(query_result) - // } + QueryResult::parser(query_result) + } #[napi] pub async fn prepare(&self, query: String) -> napi::Result { @@ -281,56 +227,36 @@ impl ScyllaSession { /// /// console.log(await session.execute("SELECT * FROM users")); /// ``` - // #[napi] - // #[allow(clippy::type_complexity)] - // pub async fn batch( - // &self, - // batch: &ScyllaBatchStatement, - // parameters: Vec< - // Option< - // Vec< - // Either10< - // u32, - // String, - // &Uuid, - // BigInt, - // &Duration, - // &Decimal, - // bool, - // Vec, - // &Float, - // HashMap< - // String, - // Either9, &Float>, - // >, - // >, - // >, - // >, - // >, - // ) -> JSQueryResult { - // let values = parameters - // .iter() - // .map(|params| { - // QueryParameter::parser(params.clone()).ok_or(napi::Error::new( - // napi::Status::InvalidArg, - // format!("Something went wrong with your batch parameters. {parameters:?}"), - // )) - // }) - // .collect::>>()?; + #[napi] + #[allow(clippy::type_complexity)] + pub async fn batch( + &self, + batch: &ScyllaBatchStatement, + parameters: Vec>>>, + ) -> JSQueryResult { + let values = parameters + .iter() + .map(|params| { + QueryParameter::parser(params.clone()).ok_or(napi::Error::new( + napi::Status::InvalidArg, + format!("Something went wrong with your batch parameters. {parameters:?}"), + )) + }) + .collect::>>()?; - // let query_result = self - // .session - // .batch(&batch.batch, values) - // .await - // .map_err(|e| { - // napi::Error::new( - // napi::Status::InvalidArg, - // format!("Something went wrong with your batch. - [{batch}] - {parameters:?}\n{e}"), - // ) - // })?; + let query_result = self + .session + .batch(&batch.batch, values) + .await + .map_err(|e| { + napi::Error::new( + napi::Status::InvalidArg, + format!("Something went wrong with your batch. - [{batch}] - {parameters:?}\n{e}"), + ) + })?; - // QueryResult::parser(query_result) - // } + QueryResult::parser(query_result) + } /// Sends `USE ` request on all connections\ /// This allows to write `SELECT * FROM table` instead of `SELECT * FROM keyspace.table`\ diff --git a/src/types/list.rs b/src/types/list.rs new file mode 100644 index 0000000..5bd74c4 --- /dev/null +++ b/src/types/list.rs @@ -0,0 +1,37 @@ +use scylla::frame::response::result::CqlValue; + +use crate::helpers::{cql_value_bridge::ParameterWithMapType, to_cql_value::ToCqlValue}; + +/// A list of any CqlType +#[napi] +#[derive(Debug, Clone)] +pub struct List { + pub(crate) inner: Vec, +} + +impl From> for List { + fn from(inner: Vec) -> Self { + Self { inner } + } +} + +impl From for Vec { + fn from(list: List) -> Self { + list.inner + } +} + +impl From<&List> for Vec { + fn from(list: &List) -> Self { + list.inner.clone() + } +} + +#[napi] +impl List { + #[napi(constructor)] + pub fn new_list(values: Vec) -> List { + let inner = values.into_iter().map(|v| v.to_cql_value()).collect(); + List { inner } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 91bba3f..ad6d45f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,6 @@ pub mod decimal; pub mod duration; pub mod float; +pub mod list; pub mod uuid; pub mod varint; From e9d3841946234ac57f353cf92590ff232c7b89f4 Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Sat, 2 Nov 2024 17:11:22 -0300 Subject: [PATCH 07/13] chore: add todo note on list type Signed-off-by: Daniel Boll --- examples/custom-types/list.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/custom-types/list.mts b/examples/custom-types/list.mts index 8cd9606..1138a4f 100644 --- a/examples/custom-types/list.mts +++ b/examples/custom-types/list.mts @@ -19,7 +19,7 @@ await session.execute( // NOTE: driver is not throwing errors if the return of the function is not used. await session.execute("INSERT INTO lists (a, b) VALUES (?, ?)", [ Uuid.randomV4(), - new List([1, 2, 3]), + new List([1, 2, 3]), // TODO: add the generic to the type annotation on index.d.ts ]); const results = await session.execute("SELECT * FROM lists"); From 4d313173493b2c0b49821d47971e574d23002dd5 Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Sat, 2 Nov 2024 17:14:32 -0300 Subject: [PATCH 08/13] chore: move code around for clarity Signed-off-by: Daniel Boll --- src/helpers/query_results.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/query_results.rs b/src/helpers/query_results.rs index 548039f..dd3201e 100644 --- a/src/helpers/query_results.rs +++ b/src/helpers/query_results.rs @@ -122,9 +122,6 @@ impl QueryResult { column.as_udt().unwrap(), field_types, )?)), - ColumnType::Custom(_) => Ok(WithMapType::A( - "ColumnType Custom not supported yet".to_string(), - )), ColumnType::List(list_type) => Ok(WithMapType::J(Self::extract_base_types( column .as_list() @@ -133,6 +130,9 @@ impl QueryResult { .map(|e| Self::parse_value(&Some(e.clone()), list_type)) .collect::>(), )?)), + ColumnType::Custom(_) => Ok(WithMapType::A( + "ColumnType Custom not supported yet".to_string(), + )), ColumnType::Set(_) => Ok(WithMapType::A( "ColumnType Set not supported yet".to_string(), )), From 27111426ea9197bf561582b4b4db4b8235da3e32 Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Sat, 2 Nov 2024 17:27:15 -0300 Subject: [PATCH 09/13] feat(types): add support for the `set` Signed-off-by: Daniel Boll --- examples/custom-types/set.mts | 13 ++++++------ examples/custom-types/tuple.mts | 0 index.d.ts | 4 ++++ index.js | 3 ++- src/helpers/cql_value_bridge.rs | 15 +++++++------ src/helpers/query_results.rs | 11 +++++++--- src/helpers/to_cql_value.rs | 9 +++++++- src/types/mod.rs | 1 + src/types/set.rs | 37 +++++++++++++++++++++++++++++++++ 9 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 examples/custom-types/tuple.mts create mode 100644 src/types/set.rs diff --git a/examples/custom-types/set.mts b/examples/custom-types/set.mts index 61b3450..1681641 100644 --- a/examples/custom-types/set.mts +++ b/examples/custom-types/set.mts @@ -1,4 +1,4 @@ -import { Cluster, Set } from "../../index.js"; +import { Cluster, Set, Uuid } from "../../index.js"; const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; @@ -13,12 +13,13 @@ await session.execute( await session.useKeyspace("sets"); await session.execute( - "CREATE TABLE IF NOT EXISTS sets (a set, primary key (a))", + "CREATE TABLE IF NOT EXISTS sets (a uuid, b set, primary key (a))", ); -await session.execute("INSERT INTO sets (a) VALUES (?)", [ - new Set([1, 2, 3]), -]); +// await session.execute("INSERT INTO sets (a, b) VALUES (?, ?)", [ +// Uuid.randomV4(), +// new Set([1, 2, 3, 1]), +// ]); -const results = await session.execute("SELECT a FROM sets"); +const results = await session.execute("SELECT * FROM sets"); console.log(results); diff --git a/examples/custom-types/tuple.mts b/examples/custom-types/tuple.mts new file mode 100644 index 0000000..e69de29 diff --git a/index.d.ts b/index.d.ts index bb841b1..97d75c1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -316,6 +316,10 @@ export class Float { export class List { constructor(values: Array) } +/** A list of any CqlType */ +export class Set { + constructor(values: Array) +} export class Uuid { /** Generates a random UUID v4. */ static randomV4(): Uuid diff --git a/index.js b/index.js index 931fa30..f4a68d1 100644 --- a/index.js +++ b/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Float, List, Uuid, Varint } = nativeBinding +const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Float, List, Set, Uuid, Varint } = nativeBinding module.exports.Compression = Compression module.exports.Consistency = Consistency @@ -327,6 +327,7 @@ module.exports.Decimal = Decimal module.exports.Duration = Duration module.exports.Float = Float module.exports.List = List +module.exports.Set = Set module.exports.Uuid = Uuid module.exports.Varint = Varint diff --git a/src/helpers/cql_value_bridge.rs b/src/helpers/cql_value_bridge.rs index 443aa8c..f374004 100644 --- a/src/helpers/cql_value_bridge.rs +++ b/src/helpers/cql_value_bridge.rs @@ -1,23 +1,24 @@ -use napi::bindgen_prelude::{BigInt, Either11, Either12}; +use napi::bindgen_prelude::{BigInt, Either12, Either13}; use scylla::frame::response::result::CqlValue; use std::collections::HashMap; use crate::types::{ - decimal::Decimal, duration::Duration, float::Float, list::List, uuid::Uuid, varint::Varint, + decimal::Decimal, duration::Duration, float::Float, list::List, set::Set, uuid::Uuid, + varint::Varint, }; use super::to_cql_value::ToCqlValue; macro_rules! define_expected_type { ($lifetime:lifetime, $($t:ty),+) => { - pub type ParameterNativeTypes<$lifetime> = Either11<$($t),+>; - pub type ParameterWithMapType<$lifetime> = Either12<$($t),+, HashMap>>; + pub type ParameterNativeTypes<$lifetime> = Either12<$($t),+>; + pub type ParameterWithMapType<$lifetime> = Either13<$($t),+, HashMap>>; pub type JSQueryParameters<$lifetime> = napi::Result>>>; }; } -define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal, bool, Vec, &'a Float, &'a Varint, &'a List); +define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal, bool, Vec, &'a Float, &'a Varint, &'a List, &'a Set); impl<'a> ToCqlValue for ParameterWithMapType<'a> { fn to_cql_value(&self) -> CqlValue { @@ -33,7 +34,8 @@ impl<'a> ToCqlValue for ParameterWithMapType<'a> { ParameterWithMapType::I(float) => float.to_cql_value(), ParameterWithMapType::J(varint) => varint.to_cql_value(), ParameterWithMapType::K(list) => list.to_cql_value(), - ParameterWithMapType::L(map) => CqlValue::UserDefinedType { + ParameterWithMapType::L(set) => set.to_cql_value(), + ParameterWithMapType::M(map) => CqlValue::UserDefinedType { // TODO: think a better way to fill this info here keyspace: "keyspace".to_string(), type_name: "type_name".to_string(), @@ -60,6 +62,7 @@ impl<'a> ToCqlValue for ParameterNativeTypes<'a> { ParameterNativeTypes::J(varint) => varint.to_cql_value(), ParameterNativeTypes::I(float) => float.to_cql_value(), ParameterNativeTypes::K(list) => list.to_cql_value(), + ParameterNativeTypes::L(set) => set.to_cql_value(), } } } diff --git a/src/helpers/query_results.rs b/src/helpers/query_results.rs index dd3201e..a713a38 100644 --- a/src/helpers/query_results.rs +++ b/src/helpers/query_results.rs @@ -130,12 +130,17 @@ impl QueryResult { .map(|e| Self::parse_value(&Some(e.clone()), list_type)) .collect::>(), )?)), + ColumnType::Set(set_type) => Ok(WithMapType::J(Self::extract_base_types( + column + .as_set() + .unwrap() + .iter() + .map(|e| Self::parse_value(&Some(e.clone()), set_type)) + .collect::>(), + )?)), ColumnType::Custom(_) => Ok(WithMapType::A( "ColumnType Custom not supported yet".to_string(), )), - ColumnType::Set(_) => Ok(WithMapType::A( - "ColumnType Set not supported yet".to_string(), - )), ColumnType::Tuple(_) => Ok(WithMapType::A( "ColumnType Tuple not supported yet".to_string(), )), diff --git a/src/helpers/to_cql_value.rs b/src/helpers/to_cql_value.rs index 6ef1a58..76f60e9 100644 --- a/src/helpers/to_cql_value.rs +++ b/src/helpers/to_cql_value.rs @@ -2,7 +2,8 @@ use napi::bindgen_prelude::BigInt; use scylla::frame::response::result::CqlValue; use crate::types::{ - decimal::Decimal, duration::Duration, float::Float, list::List, uuid::Uuid, varint::Varint, + decimal::Decimal, duration::Duration, float::Float, list::List, set::Set, uuid::Uuid, + varint::Varint, }; // Trait to abstract the conversion to CqlValue @@ -71,6 +72,12 @@ impl ToCqlValue for &List { } } +impl ToCqlValue for &Set { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Set(self.inner.clone()) + } +} + // Helper function to convert u32 vector to u8 vector fn u32_vec_to_u8_vec(input: &[u32]) -> Vec { input.iter().map(|&num| num as u8).collect() diff --git a/src/types/mod.rs b/src/types/mod.rs index ad6d45f..3cc64ef 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,5 +2,6 @@ pub mod decimal; pub mod duration; pub mod float; pub mod list; +pub mod set; pub mod uuid; pub mod varint; diff --git a/src/types/set.rs b/src/types/set.rs new file mode 100644 index 0000000..c26f49b --- /dev/null +++ b/src/types/set.rs @@ -0,0 +1,37 @@ +use scylla::frame::response::result::CqlValue; + +use crate::helpers::{cql_value_bridge::ParameterWithMapType, to_cql_value::ToCqlValue}; + +/// A list of any CqlType +#[napi] +#[derive(Debug, Clone)] +pub struct Set { + pub(crate) inner: Vec, +} + +impl From> for Set { + fn from(inner: Vec) -> Self { + Self { inner } + } +} + +impl From for Vec { + fn from(list: Set) -> Self { + list.inner + } +} + +impl From<&Set> for Vec { + fn from(list: &Set) -> Self { + list.inner.clone() + } +} + +#[napi] +impl Set { + #[napi(constructor)] + pub fn new_list(values: Vec) -> Set { + let inner = values.into_iter().map(|v| v.to_cql_value()).collect(); + Set { inner } + } +} From f595c0a5c86664794f611b8365f12f6842bc317c Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Sat, 2 Nov 2024 18:22:00 -0300 Subject: [PATCH 10/13] feat(types): add support for the `map` type also added the generation of generics Signed-off-by: Daniel Boll --- examples/custom-types/list.mts | 2 +- examples/custom-types/map.mts | 9 ++++--- examples/custom-types/set.mts | 8 +++--- examples/refactor-test.mts | 45 --------------------------------- index.d.ts | 12 ++++++--- index.js | 3 ++- scripts/fix-files.mjs | 21 ++++++++++++++- src/helpers/cql_value_bridge.rs | 14 +++++----- src/helpers/to_cql_value.rs | 8 +++++- src/types/list.rs | 2 +- src/types/map.rs | 45 +++++++++++++++++++++++++++++++++ src/types/mod.rs | 1 + src/types/set.rs | 4 +-- 13 files changed, 104 insertions(+), 70 deletions(-) delete mode 100644 examples/refactor-test.mts create mode 100644 src/types/map.rs diff --git a/examples/custom-types/list.mts b/examples/custom-types/list.mts index 1138a4f..8cd9606 100644 --- a/examples/custom-types/list.mts +++ b/examples/custom-types/list.mts @@ -19,7 +19,7 @@ await session.execute( // NOTE: driver is not throwing errors if the return of the function is not used. await session.execute("INSERT INTO lists (a, b) VALUES (?, ?)", [ Uuid.randomV4(), - new List([1, 2, 3]), // TODO: add the generic to the type annotation on index.d.ts + new List([1, 2, 3]), ]); const results = await session.execute("SELECT * FROM lists"); diff --git a/examples/custom-types/map.mts b/examples/custom-types/map.mts index 44dbb4f..828cbb6 100644 --- a/examples/custom-types/map.mts +++ b/examples/custom-types/map.mts @@ -1,4 +1,4 @@ -import { Cluster, Map } from "../../index.js"; +import { Cluster, Map, Uuid } from "../../index.js"; const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; @@ -13,10 +13,11 @@ await session.execute( await session.useKeyspace("maps"); await session.execute( - "CREATE TABLE IF NOT EXISTS maps (a map, primary key (a))", + "CREATE TABLE IF NOT EXISTS maps (a uuid, b map, primary key (a))", ); -await session.execute("INSERT INTO maps (a) VALUES (?)", [ +await session.execute("INSERT INTO maps (a, b) VALUES (?, ?)", [ + Uuid.randomV4(), new Map([ ["a", 1], ["b", 2], @@ -24,5 +25,5 @@ await session.execute("INSERT INTO maps (a) VALUES (?)", [ ]), ]); -const results = await session.execute("SELECT a FROM maps"); +const results = await session.execute("SELECT * FROM maps"); console.log(results); diff --git a/examples/custom-types/set.mts b/examples/custom-types/set.mts index 1681641..58943d6 100644 --- a/examples/custom-types/set.mts +++ b/examples/custom-types/set.mts @@ -16,10 +16,10 @@ await session.execute( "CREATE TABLE IF NOT EXISTS sets (a uuid, b set, primary key (a))", ); -// await session.execute("INSERT INTO sets (a, b) VALUES (?, ?)", [ -// Uuid.randomV4(), -// new Set([1, 2, 3, 1]), -// ]); +await session.execute("INSERT INTO sets (a, b) VALUES (?, ?)", [ + Uuid.randomV4(), + new Set([1, 2, 3, 1]), +]); const results = await session.execute("SELECT * FROM sets"); console.log(results); diff --git a/examples/refactor-test.mts b/examples/refactor-test.mts deleted file mode 100644 index 96cc540..0000000 --- a/examples/refactor-test.mts +++ /dev/null @@ -1,45 +0,0 @@ -import { Cluster, Uuid, Duration, Decimal, Float, Varint } from "../index.js"; - -const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; - -console.log(`Connecting to ${nodes}`); - -const cluster = new Cluster({ nodes }); -const session = await cluster.connect(); - -await session.execute( - "CREATE KEYSPACE IF NOT EXISTS refactor WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", -); -await session.useKeyspace("refactor"); - -await session.execute( - `CREATE TABLE IF NOT EXISTS refactor ( - a int, - b boolean, - c duration, - d decimal, - e blob, - f float, - g varint, - primary key (a) - )`, -); -// TODO: varint, map - -const a = await session - .execute( - "INSERT INTO refactor (a, b, c, d, e, f, g) VALUES (?, ?, ?, ?, ?, ?, ?)", - [ - 1, - true, - new Duration(1, 1, 1), - new Decimal([0x01, 0xe2, 0x40], 3), - Buffer.from("hello").toJSON().data, - new Float(8.3212), - ], - ) - .catch(console.error); -console.log(a); - -const results = await session.execute("SELECT * FROM refactor"); -console.log(results); diff --git a/index.d.ts b/index.d.ts index 97d75c1..ec13360 100644 --- a/index.d.ts +++ b/index.d.ts @@ -313,12 +313,16 @@ export class Float { constructor(inner: number) } /** A list of any CqlType */ -export class List { - constructor(values: Array) +export class List { + constructor(values: T[]) +} +/** A map of any CqlType to any CqlType */ +export class Map { + constructor(values: Array>) } /** A list of any CqlType */ -export class Set { - constructor(values: Array) +export class Set { + constructor(values: T[]) } export class Uuid { /** Generates a random UUID v4. */ diff --git a/index.js b/index.js index f4a68d1..a5efc61 100644 --- a/index.js +++ b/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Float, List, Set, Uuid, Varint } = nativeBinding +const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Float, List, Map, Set, Uuid, Varint } = nativeBinding module.exports.Compression = Compression module.exports.Consistency = Consistency @@ -327,6 +327,7 @@ module.exports.Decimal = Decimal module.exports.Duration = Duration module.exports.Float = Float module.exports.List = List +module.exports.Map = Map module.exports.Set = Set module.exports.Uuid = Uuid module.exports.Varint = Varint diff --git a/scripts/fix-files.mjs b/scripts/fix-files.mjs index 6f3ddbe..bf63233 100644 --- a/scripts/fix-files.mjs +++ b/scripts/fix-files.mjs @@ -1,5 +1,21 @@ import { readFileSync, writeFileSync } from "node:fs"; +function addGenericTypes(filename) { + const content = readFileSync(filename, "utf8"); + const updatedContent = content + .replace( + /export class List\b(.*){/, + "export class List$1{", + ) + .replace( + /export class Map\b(.*){/, + "export class Map$1{", + ) + .replace(/export class Set\b(.*){/, "export class Set$1{"); + + writeFileSync(filename, updatedContent); +} + // Append to filename inspectors for custom types function addInspector(filename) { writeFileSync( @@ -36,4 +52,7 @@ type JSQueryResultType = Record[]; const filename = process.argv[process.argv.length - 1]; if (filename.endsWith("index.js")) addInspector(filename); -else if (filename.endsWith("index.d.ts")) addJSQueryResultType(filename); +else if (filename.endsWith("index.d.ts")) { + addGenericTypes(filename); + addJSQueryResultType(filename); +} diff --git a/src/helpers/cql_value_bridge.rs b/src/helpers/cql_value_bridge.rs index f374004..1f69a3f 100644 --- a/src/helpers/cql_value_bridge.rs +++ b/src/helpers/cql_value_bridge.rs @@ -1,10 +1,10 @@ -use napi::bindgen_prelude::{BigInt, Either12, Either13}; +use napi::bindgen_prelude::{BigInt, Either13, Either14}; use scylla::frame::response::result::CqlValue; use std::collections::HashMap; use crate::types::{ - decimal::Decimal, duration::Duration, float::Float, list::List, set::Set, uuid::Uuid, + decimal::Decimal, duration::Duration, float::Float, list::List, map::Map, set::Set, uuid::Uuid, varint::Varint, }; @@ -12,13 +12,13 @@ use super::to_cql_value::ToCqlValue; macro_rules! define_expected_type { ($lifetime:lifetime, $($t:ty),+) => { - pub type ParameterNativeTypes<$lifetime> = Either12<$($t),+>; - pub type ParameterWithMapType<$lifetime> = Either13<$($t),+, HashMap>>; + pub type ParameterNativeTypes<$lifetime> = Either13<$($t),+>; + pub type ParameterWithMapType<$lifetime> = Either14<$($t),+, HashMap>>; pub type JSQueryParameters<$lifetime> = napi::Result>>>; }; } -define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal, bool, Vec, &'a Float, &'a Varint, &'a List, &'a Set); +define_expected_type!('a, u32, String, &'a Uuid, BigInt, &'a Duration, &'a Decimal, bool, Vec, &'a Float, &'a Varint, &'a List, &'a Set, &'a Map); impl<'a> ToCqlValue for ParameterWithMapType<'a> { fn to_cql_value(&self) -> CqlValue { @@ -35,7 +35,8 @@ impl<'a> ToCqlValue for ParameterWithMapType<'a> { ParameterWithMapType::J(varint) => varint.to_cql_value(), ParameterWithMapType::K(list) => list.to_cql_value(), ParameterWithMapType::L(set) => set.to_cql_value(), - ParameterWithMapType::M(map) => CqlValue::UserDefinedType { + ParameterWithMapType::M(map) => map.to_cql_value(), + ParameterWithMapType::N(map) => CqlValue::UserDefinedType { // TODO: think a better way to fill this info here keyspace: "keyspace".to_string(), type_name: "type_name".to_string(), @@ -63,6 +64,7 @@ impl<'a> ToCqlValue for ParameterNativeTypes<'a> { ParameterNativeTypes::I(float) => float.to_cql_value(), ParameterNativeTypes::K(list) => list.to_cql_value(), ParameterNativeTypes::L(set) => set.to_cql_value(), + ParameterNativeTypes::M(map) => map.to_cql_value(), } } } diff --git a/src/helpers/to_cql_value.rs b/src/helpers/to_cql_value.rs index 76f60e9..3ff4e52 100644 --- a/src/helpers/to_cql_value.rs +++ b/src/helpers/to_cql_value.rs @@ -2,7 +2,7 @@ use napi::bindgen_prelude::BigInt; use scylla::frame::response::result::CqlValue; use crate::types::{ - decimal::Decimal, duration::Duration, float::Float, list::List, set::Set, uuid::Uuid, + decimal::Decimal, duration::Duration, float::Float, list::List, map::Map, set::Set, uuid::Uuid, varint::Varint, }; @@ -78,6 +78,12 @@ impl ToCqlValue for &Set { } } +impl ToCqlValue for &Map { + fn to_cql_value(&self) -> CqlValue { + CqlValue::Map(self.inner.clone()) + } +} + // Helper function to convert u32 vector to u8 vector fn u32_vec_to_u8_vec(input: &[u32]) -> Vec { input.iter().map(|&num| num as u8).collect() diff --git a/src/types/list.rs b/src/types/list.rs index 5bd74c4..36737f2 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -29,7 +29,7 @@ impl From<&List> for Vec { #[napi] impl List { - #[napi(constructor)] + #[napi(constructor, ts_args_type = "values: T[]")] pub fn new_list(values: Vec) -> List { let inner = values.into_iter().map(|v| v.to_cql_value()).collect(); List { inner } diff --git a/src/types/map.rs b/src/types/map.rs new file mode 100644 index 0000000..2d3e5f1 --- /dev/null +++ b/src/types/map.rs @@ -0,0 +1,45 @@ +use scylla::frame::response::result::CqlValue; + +use crate::helpers::{cql_value_bridge::ParameterWithMapType, to_cql_value::ToCqlValue}; + +/// A map of any CqlType to any CqlType +#[napi] +#[derive(Debug, Clone)] +pub struct Map { + pub(crate) inner: Vec<(CqlValue, CqlValue)>, +} + +impl From> for Map { + fn from(inner: Vec<(CqlValue, CqlValue)>) -> Self { + Self { inner } + } +} + +impl From for Vec<(CqlValue, CqlValue)> { + fn from(map: Map) -> Self { + map.inner + } +} + +impl From<&Map> for Vec<(CqlValue, CqlValue)> { + fn from(map: &Map) -> Self { + map.inner.clone() + } +} + +#[napi] +impl Map { + #[napi(constructor, ts_args_type = "values: Array>")] + pub fn new_map(values: Vec>) -> Map { + Map { + inner: values + .into_iter() + .map(|v| { + let key = v[0].to_cql_value(); + let value = v[1].to_cql_value(); + (key, value) + }) + .collect(), + } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 3cc64ef..11744a2 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,6 +2,7 @@ pub mod decimal; pub mod duration; pub mod float; pub mod list; +pub mod map; pub mod set; pub mod uuid; pub mod varint; diff --git a/src/types/set.rs b/src/types/set.rs index c26f49b..97b0e88 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -29,8 +29,8 @@ impl From<&Set> for Vec { #[napi] impl Set { - #[napi(constructor)] - pub fn new_list(values: Vec) -> Set { + #[napi(constructor, ts_args_type = "values: T[]")] + pub fn new_set(values: Vec) -> Set { let inner = values.into_iter().map(|v| v.to_cql_value()).collect(); Set { inner } } From 36635a9f500498544df85177de807f13eea78633 Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Sat, 2 Nov 2024 19:02:59 -0300 Subject: [PATCH 11/13] feat(tracing): add tracing in this refactor Signed-off-by: Daniel Boll --- Cargo.toml | 1 + examples/tracing.mts | 18 +++++ index.d.ts | 1 + src/helpers/query_results.rs | 2 +- src/session/scylla_session.rs | 142 ++++++++++++++++++++++++++++++++-- src/types/decimal.rs | 2 +- src/types/duration.rs | 2 +- src/types/float.rs | 2 +- src/types/list.rs | 2 +- src/types/map.rs | 2 +- src/types/mod.rs | 1 + src/types/set.rs | 2 +- src/types/tracing.rs | 91 ++++++++++++++++++++++ src/types/uuid.rs | 2 +- src/types/varint.rs | 2 +- 15 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 examples/tracing.mts create mode 100644 src/types/tracing.rs diff --git a/Cargo.toml b/Cargo.toml index c16ea9b..8bed29f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ scylla = { version = "0.13.1", features = [ ] } uuid = { version = "1.4.1", features = ["serde", "v4", "fast-rng"] } serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } openssl = { version = "0.10", features = ["vendored"] } [build-dependencies] diff --git a/examples/tracing.mts b/examples/tracing.mts new file mode 100644 index 0000000..74ce275 --- /dev/null +++ b/examples/tracing.mts @@ -0,0 +1,18 @@ +import { Cluster } from "../index.js"; + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"]; + +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ nodes }); +const session = await cluster.connect(); + +const { tracing } = await session.executeWithTracing( + "SELECT * FROM system_schema.scylla_tables", + [], + // { + // prepare: true, + // }, +); + +console.log(tracing); diff --git a/index.d.ts b/index.d.ts index ec13360..85e651a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -157,6 +157,7 @@ export class Metrics { export class ScyllaSession { metrics(): Metrics getClusterData(): Promise + executeWithTracing(query: string | Query | PreparedStatement, parameters?: Array | undefined | null, options?: QueryOptions | undefined | null): Promise /** * Sends a query to the database and receives a response.\ * Returns only a single page of results, to receive multiple pages use (TODO: Not implemented yet) diff --git a/src/helpers/query_results.rs b/src/helpers/query_results.rs index a713a38..dfef807 100644 --- a/src/helpers/query_results.rs +++ b/src/helpers/query_results.rs @@ -12,7 +12,7 @@ macro_rules! define_return_type { ($($t:ty),+) => { type BaseTypes = Either9<$($t),+>; type NativeTypes = Either10<$($t),+, Vec>; - type WithMapType = Either11<$($t),+, Vec, HashMap>; + pub type WithMapType = Either11<$($t),+, Vec, HashMap>; type ReturnType = napi::Result>; pub type JSQueryResult = napi::Result>>; }; diff --git a/src/session/scylla_session.rs b/src/session/scylla_session.rs index 7756810..d287dd6 100644 --- a/src/session/scylla_session.rs +++ b/src/session/scylla_session.rs @@ -4,9 +4,11 @@ use crate::helpers::query_results::{JSQueryResult, QueryResult}; use crate::query::batch_statement::ScyllaBatchStatement; use crate::query::scylla_prepared_statement::PreparedStatement; use crate::query::scylla_query::Query; +use crate::types::tracing::TracingReturn; use crate::types::uuid::Uuid; use napi::Either; use napi::bindgen_prelude::Either3; +use scylla::statement::query::Query as ScyllaQuery; use super::metrics; use super::topology::ScyllaClusterData; @@ -44,6 +46,61 @@ impl ScyllaSession { cluster_data.into() } + #[napi] + pub async fn execute_with_tracing( + &self, + query: Either3, + parameters: Option>>, + options: Option, + ) -> napi::Result { + let values = QueryParameter::parser(parameters.clone()).ok_or_else(|| { + napi::Error::new( + napi::Status::InvalidArg, + format!( + "Something went wrong with your query parameters. {:?}", + parameters + ), + ) + })?; + + let should_prepare = options.map_or(false, |options| options.prepare.unwrap_or(false)); + + match query { + Either3::A(ref query_str) if should_prepare => { + let mut prepared = self.session.prepare(query_str.clone()).await.map_err(|e| { + napi::Error::new( + napi::Status::InvalidArg, + format!( + "Something went wrong preparing your statement. - [{}]\n{}", + query_str, e + ), + ) + })?; + prepared.set_tracing(true); + self.execute_prepared(&prepared, values, query_str).await + } + Either3::A(query_str) => { + let mut query = ScyllaQuery::new(query_str); + query.set_tracing(true); + self.execute_query(Either::B(query), values).await + } + Either3::B(query_ref) => { + let mut query = query_ref.query.clone(); + query.set_tracing(true); + + self.execute_query(Either::B(query), values).await + } + Either3::C(prepared_ref) => { + let mut prepared = prepared_ref.prepared.clone(); + prepared.set_tracing(true); + + self + .execute_prepared(&prepared, values, prepared_ref.prepared.get_statement()) + .await + } + } + } + /// Sends a query to the database and receives a response.\ /// Returns only a single page of results, to receive multiple pages use (TODO: Not implemented yet) /// @@ -77,7 +134,7 @@ impl ScyllaSession { let should_prepare = options.map_or(false, |options| options.prepare.unwrap_or(false)); - match query { + let a = match query { Either3::A(ref query_str) if should_prepare => { let prepared = self.session.prepare(query_str.clone()).await.map_err(|e| { napi::Error::new( @@ -106,6 +163,23 @@ impl ScyllaSession { .await } } + .map_err(|e| { + napi::Error::new( + napi::Status::InvalidArg, + format!("Something went wrong with your query. - \n{}", e), // TODO: handle different queries here + ) + })? + .get("result") + .cloned() + .ok_or(napi::Error::new( + napi::Status::InvalidArg, + r#"Something went wrong with your query."#.to_string(), // TODO: handle different queries here + ))?; + + match a { + Either::A(results) => Ok(results), + Either::B(_tracing) => unreachable!(), + } } // Helper method to handle prepared statements @@ -114,7 +188,7 @@ impl ScyllaSession { prepared: &scylla::prepared_statement::PreparedStatement, values: QueryParameter<'_>, query: &str, - ) -> JSQueryResult { + ) -> napi::Result { let query_result = self.session.execute(prepared, values).await.map_err(|e| { napi::Error::new( napi::Status::InvalidArg, @@ -124,7 +198,33 @@ impl ScyllaSession { ), ) })?; - QueryResult::parser(query_result) + + let tracing = if let Some(tracing_id) = query_result.tracing_id { + Some(crate::types::tracing::TracingInfo::from( + self + .session + .get_tracing_info(&tracing_id) + .await + .map_err(|e| { + napi::Error::new( + napi::Status::InvalidArg, + format!( + "Something went wrong with your tracing info. - [{}]\n{}", + query, e + ), + ) + })?, + )) + } else { + None + }; + + let result = QueryResult::parser(query_result)?; + + Ok(TracingReturn::from([ + ("result".to_string(), Either::A(result)), + ("tracing".to_string(), Either::B(tracing.into())), + ])) } // Helper method to handle direct queries @@ -132,13 +232,13 @@ impl ScyllaSession { &self, query: Either, values: QueryParameter<'_>, - ) -> JSQueryResult { + ) -> napi::Result { let query_result = match &query { Either::A(query_str) => self.session.query(query_str.clone(), values).await, Either::B(query_ref) => self.session.query(query_ref.clone(), values).await, } .map_err(|e| { - let query_str = match query { + let query_str = match query.clone() { Either::A(query_str) => query_str, Either::B(query_ref) => query_ref.contents.clone(), }; @@ -151,7 +251,37 @@ impl ScyllaSession { ) })?; - QueryResult::parser(query_result) + let tracing_info = if let Some(tracing_id) = query_result.tracing_id { + Some(crate::types::tracing::TracingInfo::from( + self + .session + .get_tracing_info(&tracing_id) + .await + .map_err(|e| { + napi::Error::new( + napi::Status::InvalidArg, + format!( + "Something went wrong with your tracing info. - [{}]\n{}", + match query { + Either::A(query_str) => query_str, + Either::B(query_ref) => query_ref.contents.clone(), + }, + e + ), + ) + })?, + )) + } else { + None + }; + + Ok(TracingReturn::from([ + ( + "result".to_string(), + Either::A(QueryResult::parser(query_result)?), + ), + ("tracing".to_string(), Either::B(tracing_info.into())), + ])) } #[allow(clippy::type_complexity)] diff --git a/src/types/decimal.rs b/src/types/decimal.rs index da9dc45..3b7acba 100644 --- a/src/types/decimal.rs +++ b/src/types/decimal.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use scylla::frame::value::CqlDecimal; #[napi] -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct Decimal { int_val: Vec, scale: i32, diff --git a/src/types/duration.rs b/src/types/duration.rs index 20eefaa..ec8e7e5 100644 --- a/src/types/duration.rs +++ b/src/types/duration.rs @@ -1,7 +1,7 @@ use scylla::frame::value::CqlDuration; #[napi] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Duration { pub months: i32, pub days: i32, diff --git a/src/types/float.rs b/src/types/float.rs index b829cf4..c7eff8f 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -3,7 +3,7 @@ /// Due to the nature of numbers in JavaScript, it's hard to distinguish between integers and floats, so this type is used to represent /// float numbers while any other JS number will be treated as an integer. (This is not the case for BigInts, which are always treated as BigInts). #[napi] -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct Float { pub(crate) inner: f64, } diff --git a/src/types/list.rs b/src/types/list.rs index 36737f2..984a1cd 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -4,7 +4,7 @@ use crate::helpers::{cql_value_bridge::ParameterWithMapType, to_cql_value::ToCql /// A list of any CqlType #[napi] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct List { pub(crate) inner: Vec, } diff --git a/src/types/map.rs b/src/types/map.rs index 2d3e5f1..28c9e25 100644 --- a/src/types/map.rs +++ b/src/types/map.rs @@ -4,7 +4,7 @@ use crate::helpers::{cql_value_bridge::ParameterWithMapType, to_cql_value::ToCql /// A map of any CqlType to any CqlType #[napi] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Map { pub(crate) inner: Vec<(CqlValue, CqlValue)>, } diff --git a/src/types/mod.rs b/src/types/mod.rs index 11744a2..4ad93c1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -4,5 +4,6 @@ pub mod float; pub mod list; pub mod map; pub mod set; +pub mod tracing; pub mod uuid; pub mod varint; diff --git a/src/types/set.rs b/src/types/set.rs index 97b0e88..1218f0f 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -4,7 +4,7 @@ use crate::helpers::{cql_value_bridge::ParameterWithMapType, to_cql_value::ToCql /// A list of any CqlType #[napi] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Set { pub(crate) inner: Vec, } diff --git a/src/types/tracing.rs b/src/types/tracing.rs new file mode 100644 index 0000000..ea4a5d1 --- /dev/null +++ b/src/types/tracing.rs @@ -0,0 +1,91 @@ +use std::{collections::HashMap, net::IpAddr}; + +use napi::Either; +use serde::Serialize; + +use crate::helpers::query_results::WithMapType; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CqlTimestampWrapper(pub scylla::frame::value::CqlTimestamp); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CqlTimeuuidWrapper(pub scylla::frame::value::CqlTimeuuid); + +impl Serialize for CqlTimestampWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_i64(self.0.0) + } +} + +impl Serialize for CqlTimeuuidWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(format!("{}", self.0).as_str()) + } +} + +/// Tracing info retrieved from `system_traces.sessions` +/// with all events from `system_traces.events` +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct TracingInfo { + pub client: Option, + pub command: Option, + pub coordinator: Option, + pub duration: Option, + pub parameters: Option>, + pub request: Option, + /// started_at is a timestamp - time since unix epoch + pub started_at: Option, + + pub events: Vec, +} + +/// A single event happening during a traced query +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct TracingEvent { + pub event_id: CqlTimeuuidWrapper, + pub activity: Option, + pub source: Option, + pub source_elapsed: Option, + pub thread: Option, +} + +impl From for serde_json::Value { + fn from(info: TracingInfo) -> Self { + serde_json::json!(info) + } +} + +impl From for TracingInfo { + fn from(info: scylla::tracing::TracingInfo) -> Self { + Self { + client: info.client, + command: info.command, + coordinator: info.coordinator, + duration: info.duration, + parameters: info.parameters, + request: info.request, + started_at: info.started_at.map(CqlTimestampWrapper), + events: info.events.into_iter().map(TracingEvent::from).collect(), + } + } +} + +impl From for TracingEvent { + fn from(event: scylla::tracing::TracingEvent) -> Self { + Self { + event_id: CqlTimeuuidWrapper(event.event_id), + activity: event.activity, + source: event.source, + source_elapsed: event.source_elapsed, + thread: event.thread, + } + } +} + +pub type TracingReturn = + HashMap>, serde_json::Value>>; diff --git a/src/types/uuid.rs b/src/types/uuid.rs index e0abc16..2dc8600 100644 --- a/src/types/uuid.rs +++ b/src/types/uuid.rs @@ -2,7 +2,7 @@ use napi::Result; use scylla::frame::value::CqlTimeuuid; #[napi] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Uuid { pub(crate) uuid: uuid::Uuid, } diff --git a/src/types/varint.rs b/src/types/varint.rs index a6983ad..d95be31 100644 --- a/src/types/varint.rs +++ b/src/types/varint.rs @@ -16,7 +16,7 @@ use scylla::frame::value::CqlVarint; /// Currently, Scylla and Cassandra support non-normalized `varint` values. /// Bytes provided by the user via constructor are passed to DB as is. #[napi] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Varint { pub(crate) inner: Vec, } From e01ad9cb63fa6912b500a6b1f4414ac521d04751 Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Sat, 2 Nov 2024 19:41:54 -0300 Subject: [PATCH 12/13] fix(types): fix type generation and information about tracing Signed-off-by: Daniel Boll --- examples/tracing.mts | 4 +- index.d.ts | 374 ++++++++++++++---------- index.js | 659 +++++++++++++++++++++++------------------- scripts/fix-files.mjs | 49 +++- 4 files changed, 610 insertions(+), 476 deletions(-) diff --git a/examples/tracing.mts b/examples/tracing.mts index 74ce275..9ad76d2 100644 --- a/examples/tracing.mts +++ b/examples/tracing.mts @@ -7,7 +7,7 @@ console.log(`Connecting to ${nodes}`); const cluster = new Cluster({ nodes }); const session = await cluster.connect(); -const { tracing } = await session.executeWithTracing( +const { tracing, result } = await session.executeWithTracing( "SELECT * FROM system_schema.scylla_tables", [], // { @@ -15,4 +15,4 @@ const { tracing } = await session.executeWithTracing( // }, ); -console.log(tracing); +console.log(result, tracing); diff --git a/index.d.ts b/index.d.ts index 85e651a..84a2057 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,112 +1,5 @@ -/* tslint:disable */ -/* eslint-disable */ - /* auto-generated by NAPI-RS */ - -export const enum Compression { - None = 0, - Lz4 = 1, - Snappy = 2 -} -export interface ClusterConfig { - nodes: Array - compression?: Compression - defaultExecutionProfile?: ExecutionProfile - keyspace?: string - auth?: Auth - ssl?: Ssl - /** The driver automatically awaits schema agreement after a schema-altering query is executed. Waiting for schema agreement more than necessary is never a bug, but might slow down applications which do a lot of schema changes (e.g. a migration). For instance, in case where somebody wishes to create a keyspace and then a lot of tables in it, it makes sense only to wait after creating a keyspace and after creating all the tables rather than after every query. */ - autoAwaitSchemaAgreement?: boolean - /** If the schema is not agreed upon, the driver sleeps for a duration in seconds before checking it again. The default value is 0.2 (200 milliseconds) */ - schemaAgreementInterval?: number -} -export const enum Consistency { - Any = 0, - One = 1, - Two = 2, - Three = 3, - Quorum = 4, - All = 5, - LocalQuorum = 6, - EachQuorum = 7, - LocalOne = 10, - Serial = 8, - LocalSerial = 9 -} -export const enum SerialConsistency { - Serial = 8, - LocalSerial = 9 -} -export interface ExecutionProfile { - consistency?: Consistency - serialConsistency?: SerialConsistency - requestTimeout?: number -} -export interface ConnectionOptions { - keyspace?: string - auth?: Auth - ssl?: Ssl -} -export interface Auth { - username: string - password: string -} -export interface Ssl { - enabled: boolean - caFilepath?: string - privateKeyFilepath?: string - truststoreFilepath?: string - verifyMode?: VerifyMode -} -export const enum VerifyMode { - None = 0, - Peer = 1 -} -export interface QueryOptions { - prepare?: boolean -} -export interface ScyllaKeyspace { - strategy: ScyllaStrategy - tables: Record - views: Record -} -export interface ScyllaStrategy { - kind: string - data?: SimpleStrategy | NetworkTopologyStrategy | Other -} -export interface SimpleStrategy { - replicationFactor: number -} -export interface NetworkTopologyStrategy { - datacenterRepfactors: Record -} -export interface Other { - name: string - data: Record -} -export interface ScyllaTable { - columns: Array - partitionKey: Array - clusteringKey: Array - partitioner?: string -} -export interface ScyllaMaterializedView { - viewMetadata: ScyllaTable - baseTableName: string -} -export type ScyllaCluster = Cluster -export class Cluster { - /** - * Object config is in the format: - * { - * nodes: Array, - * } - */ - constructor(clusterConfig: ClusterConfig) - /** Connect to the cluster */ - connect(keyspaceOrOptions?: string | ConnectionOptions | undefined | null, options?: ConnectionOptions | undefined | null): Promise -} -export type ScyllaBatchStatement = BatchStatement +/* eslint-disable */ /** * Batch statements * @@ -114,7 +7,7 @@ export type ScyllaBatchStatement = BatchStatement * These statements can be simple or prepared. * Only INSERT, UPDATE and DELETE statements are allowed. */ -export class BatchStatement { +export declare class BatchStatement { constructor() /** * Appends a statement to the batch. @@ -124,17 +17,57 @@ export class BatchStatement { */ appendStatement(statement: Query | PreparedStatement): void } -export class PreparedStatement { - setConsistency(consistency: Consistency): void - setSerialConsistency(serialConsistency: SerialConsistency): void +export type ScyllaBatchStatement = BatchStatement + +export declare class Cluster { + /** + * Object config is in the format: + * { + * nodes: Array, + * } + */ + constructor(clusterConfig: ClusterConfig) + /** Connect to the cluster */ + connect(keyspaceOrOptions?: string | ConnectionOptions | undefined | null, options?: ConnectionOptions | undefined | null): Promise } -export class Query { - constructor(query: string) - setConsistency(consistency: Consistency): void - setSerialConsistency(serialConsistency: SerialConsistency): void - setPageSize(pageSize: number): void +export type ScyllaCluster = Cluster + +export declare class Decimal { + constructor(intVal: Array, scale: number) + /** Returns the string representation of the Decimal. */ + toString(): string +} + +export declare class Duration { + months: number + days: number + nanoseconds: number + constructor(months: number, days: number, nanoseconds: number) + /** Returns the string representation of the Duration. */ + toString(): string } -export class Metrics { + +/** + * A float number. + * + * Due to the nature of numbers in JavaScript, it's hard to distinguish between integers and floats, so this type is used to represent + * float numbers while any other JS number will be treated as an integer. (This is not the case for BigInts, which are always treated as BigInts). + */ +export declare class Float { + constructor(inner: number) +} + +/** A list of any CqlType */ +export declare class List { + constructor(values: T[]) +} + +/** A map of any CqlType to any CqlType */ +export declare class Map { + constructor(values: Array>) +} + +export declare class Metrics { /** Returns counter for nonpaged queries */ getQueriesNum(): bigint /** Returns counter for pages requested in paged queries */ @@ -154,7 +87,28 @@ export class Metrics { */ getLatencyPercentileMs(percentile: number): bigint } -export class ScyllaSession { + +export declare class PreparedStatement { + setConsistency(consistency: Consistency): void + setSerialConsistency(serialConsistency: SerialConsistency): void +} + +export declare class Query { + constructor(query: string) + setConsistency(consistency: Consistency): void + setSerialConsistency(serialConsistency: SerialConsistency): void + setPageSize(pageSize: number): void +} + +export declare class ScyllaClusterData { + /** + * Access keyspaces details collected by the driver Driver collects various schema details like + * tables, partitioners, columns, types. They can be read using this method + */ + getKeyspaceInfo(): Record | null +} + +export declare class ScyllaSession { metrics(): Metrics getClusterData(): Promise executeWithTracing(query: string | Query | PreparedStatement, parameters?: Array | undefined | null, options?: QueryOptions | undefined | null): Promise @@ -284,48 +238,13 @@ export class ScyllaSession { awaitSchemaAgreement(): Promise checkSchemaAgreement(): Promise } -export class ScyllaClusterData { - /** - * Access keyspaces details collected by the driver Driver collects various schema details like - * tables, partitioners, columns, types. They can be read using this method - */ - getKeyspaceInfo(): Record | null -} -export class Decimal { - constructor(intVal: Array, scale: number) - /** Returns the string representation of the Decimal. */ - toString(): string -} -export class Duration { - months: number - days: number - nanoseconds: number - constructor(months: number, days: number, nanoseconds: number) - /** Returns the string representation of the Duration. */ - toString(): string -} -/** - * A float number. - * - * Due to the nature of numbers in JavaScript, it's hard to distinguish between integers and floats, so this type is used to represent - * float numbers while any other JS number will be treated as an integer. (This is not the case for BigInts, which are always treated as BigInts). - */ -export class Float { - constructor(inner: number) -} -/** A list of any CqlType */ -export class List { - constructor(values: T[]) -} -/** A map of any CqlType to any CqlType */ -export class Map { - constructor(values: Array>) -} + /** A list of any CqlType */ -export class Set { +export declare class Set { constructor(values: T[]) } -export class Uuid { + +export declare class Uuid { /** Generates a random UUID v4. */ static randomV4(): Uuid /** Parses a UUID from a string. It may fail if the string is not a valid UUID. */ @@ -333,6 +252,7 @@ export class Uuid { /** Returns the string representation of the UUID. */ toString(): string } + /** * Native CQL `varint` representation. * @@ -350,11 +270,145 @@ export class Uuid { * Currently, Scylla and Cassandra support non-normalized `varint` values. * Bytes provided by the user via constructor are passed to DB as is. */ -export class Varint { +export declare class Varint { constructor(inner: Array) } +export interface Auth { + username: string + password: string +} + +export interface ClusterConfig { + nodes: Array + compression?: Compression + defaultExecutionProfile?: ExecutionProfile + keyspace?: string + auth?: Auth + ssl?: Ssl + /** The driver automatically awaits schema agreement after a schema-altering query is executed. Waiting for schema agreement more than necessary is never a bug, but might slow down applications which do a lot of schema changes (e.g. a migration). For instance, in case where somebody wishes to create a keyspace and then a lot of tables in it, it makes sense only to wait after creating a keyspace and after creating all the tables rather than after every query. */ + autoAwaitSchemaAgreement?: boolean + /** If the schema is not agreed upon, the driver sleeps for a duration in seconds before checking it again. The default value is 0.2 (200 milliseconds) */ + schemaAgreementInterval?: number +} + +export declare const enum Compression { + None = 0, + Lz4 = 1, + Snappy = 2 +} + +export interface ConnectionOptions { + keyspace?: string + auth?: Auth + ssl?: Ssl +} + +export declare const enum Consistency { + Any = 0, + One = 1, + Two = 2, + Three = 3, + Quorum = 4, + All = 5, + LocalQuorum = 6, + EachQuorum = 7, + LocalOne = 10, + Serial = 8, + LocalSerial = 9 +} + +export interface ExecutionProfile { + consistency?: Consistency + serialConsistency?: SerialConsistency + requestTimeout?: number +} + +export interface NetworkTopologyStrategy { + datacenterRepfactors: Record +} + +export interface Other { + name: string + data: Record +} + +export interface QueryOptions { + prepare?: boolean +} + +export interface ScyllaKeyspace { + strategy: ScyllaStrategy + tables: Record + views: Record +} + +export interface ScyllaMaterializedView { + viewMetadata: ScyllaTable + baseTableName: string +} + +export interface ScyllaStrategy { + kind: string + data?: SimpleStrategy | NetworkTopologyStrategy | Other +} + +export interface ScyllaTable { + columns: Array + partitionKey: Array + clusteringKey: Array + partitioner?: string +} + +export declare const enum SerialConsistency { + Serial = 8, + LocalSerial = 9 +} + +export interface SimpleStrategy { + replicationFactor: number +} + +export interface Ssl { + enabled: boolean + caFilepath?: string + privateKeyFilepath?: string + truststoreFilepath?: string + verifyMode?: VerifyMode +} + +export declare const enum VerifyMode { + None = 0, + Peer = 1 +} + type NativeTypes = number | string | Uuid | bigint | Duration | Decimal | Float | List; type WithMapType = NativeTypes | Record | NativeTypes[]; type ParameterWithMapType = WithMapType; -type JSQueryResultType = Record[]; \ No newline at end of file +type JSQueryResult = Record[]; +type TracingReturn = { result: JSQueryResult; tracing: TracingInfo }; + +export interface TracingInfo { + client?: string; // IP address as a string + command?: string; + coordinator?: string; // IP address as a string + duration?: number; + parameters?: Record; + request?: string; + /** + * started_at is a timestamp - time since unix epoch + */ + started_at?: string; + events: TracingEvent[]; +} + +/** + * A single event happening during a traced query + */ +export interface TracingEvent { + event_id: string; + activity?: string; + source?: string; // IP address as a string + source_elapsed?: number; + thread?: string; +} \ No newline at end of file diff --git a/index.js b/index.js index a5efc61..111a2f6 100644 --- a/index.js +++ b/index.js @@ -1,339 +1,390 @@ -/* tslint:disable */ +// prettier-ignore /* eslint-disable */ -/* prettier-ignore */ - /* auto-generated by NAPI-RS */ -const { existsSync, readFileSync } = require('fs') -const { join } = require('path') - -const { platform, arch } = process +const { readFileSync } = require('fs') let nativeBinding = null -let localFileExisted = false -let loadError = null +const loadErrors = [] -function isMusl() { - // For Node 10 - if (!process.report || typeof process.report.getReport !== 'function') { - try { - const lddPath = require('child_process').execSync('which ldd').toString().trim() - return readFileSync(lddPath, 'utf8').includes('musl') - } catch (e) { +const isMusl = () => { + let musl = false + if (process.platform === 'linux') { + musl = isMuslFromFilesystem() + if (musl === null) { + musl = isMuslFromReport() + } + if (musl === null) { + musl = isMuslFromChildProcess() + } + } + return musl +} + +const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') + +const isMuslFromFilesystem = () => { + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') + } catch { + return null + } +} + +const isMuslFromReport = () => { + const report = typeof process.report.getReport === 'function' ? process.report.getReport() : null + if (!report) { + return null + } + if (report.header && report.header.glibcVersionRuntime) { + return false + } + if (Array.isArray(report.sharedObjects)) { + if (report.sharedObjects.some(isFileMusl)) { return true } - } else { - const { glibcVersionRuntime } = process.report.getReport().header - return !glibcVersionRuntime } + return false } -switch (platform) { - case 'android': - switch (arch) { - case 'arm64': - localFileExisted = existsSync(join(__dirname, 'scylladb.android-arm64.node')) +const isMuslFromChildProcess = () => { + try { + return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') + } catch (e) { + // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false + return false + } +} + +function requireNative() { + if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./scylladb.android-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-android-arm64') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm') { + try { + return require('./scylladb.android-arm-eabi.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-android-arm-eabi') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) + } + } else if (process.platform === 'win32') { + if (process.arch === 'x64') { + try { + return require('./scylladb.win32-x64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-win32-x64-msvc') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'ia32') { + try { + return require('./scylladb.win32-ia32-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-win32-ia32-msvc') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm64') { + try { + return require('./scylladb.win32-arm64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-win32-arm64-msvc') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) + } + } else if (process.platform === 'darwin') { + try { + return require('./scylladb.darwin-universal.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-darwin-universal') + } catch (e) { + loadErrors.push(e) + } + + if (process.arch === 'x64') { + try { + return require('./scylladb.darwin-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-darwin-x64') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm64') { + try { + return require('./scylladb.darwin-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-darwin-arm64') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) + } + } else if (process.platform === 'freebsd') { + if (process.arch === 'x64') { + try { + return require('./scylladb.freebsd-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-freebsd-x64') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm64') { + try { + return require('./scylladb.freebsd-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-freebsd-arm64') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) + } + } else if (process.platform === 'linux') { + if (process.arch === 'x64') { + if (isMusl()) { try { - if (localFileExisted) { - nativeBinding = require('./scylladb.android-arm64.node') - } else { - nativeBinding = require('@lambda-group/scylladb-android-arm64') - } - } catch (e) { - loadError = e - } - break - case 'arm': - localFileExisted = existsSync(join(__dirname, 'scylladb.android-arm-eabi.node')) + return require('./scylladb.linux-x64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-x64-musl') + } catch (e) { + loadErrors.push(e) + } + + } else { try { - if (localFileExisted) { - nativeBinding = require('./scylladb.android-arm-eabi.node') - } else { - nativeBinding = require('@lambda-group/scylladb-android-arm-eabi') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on Android ${arch}`) - } - break - case 'win32': - switch (arch) { - case 'x64': - localFileExisted = existsSync( - join(__dirname, 'scylladb.win32-x64-msvc.node') - ) + return require('./scylladb.linux-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-x64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'arm64') { + if (isMusl()) { try { - if (localFileExisted) { - nativeBinding = require('./scylladb.win32-x64-msvc.node') - } else { - nativeBinding = require('@lambda-group/scylladb-win32-x64-msvc') - } - } catch (e) { - loadError = e - } - break - case 'ia32': - localFileExisted = existsSync( - join(__dirname, 'scylladb.win32-ia32-msvc.node') - ) + return require('./scylladb.linux-arm64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-arm64-musl') + } catch (e) { + loadErrors.push(e) + } + + } else { try { - if (localFileExisted) { - nativeBinding = require('./scylladb.win32-ia32-msvc.node') - } else { - nativeBinding = require('@lambda-group/scylladb-win32-ia32-msvc') - } - } catch (e) { - loadError = e - } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'scylladb.win32-arm64-msvc.node') - ) + return require('./scylladb.linux-arm64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-arm64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'arm') { + if (isMusl()) { try { - if (localFileExisted) { - nativeBinding = require('./scylladb.win32-arm64-msvc.node') - } else { - nativeBinding = require('@lambda-group/scylladb-win32-arm64-msvc') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on Windows: ${arch}`) - } - break - case 'darwin': - localFileExisted = existsSync(join(__dirname, 'scylladb.darwin-universal.node')) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.darwin-universal.node') + return require('./scylladb.linux-arm-musleabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-arm-musleabihf') + } catch (e) { + loadErrors.push(e) + } + } else { - nativeBinding = require('@lambda-group/scylladb-darwin-universal') + try { + return require('./scylladb.linux-arm-gnueabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-arm-gnueabihf') + } catch (e) { + loadErrors.push(e) } - break - } catch {} - switch (arch) { - case 'x64': - localFileExisted = existsSync(join(__dirname, 'scylladb.darwin-x64.node')) + + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { try { - if (localFileExisted) { - nativeBinding = require('./scylladb.darwin-x64.node') - } else { - nativeBinding = require('@lambda-group/scylladb-darwin-x64') - } - } catch (e) { - loadError = e - } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'scylladb.darwin-arm64.node') - ) + return require('./scylladb.linux-riscv64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-riscv64-musl') + } catch (e) { + loadErrors.push(e) + } + + } else { try { - if (localFileExisted) { - nativeBinding = require('./scylladb.darwin-arm64.node') - } else { - nativeBinding = require('@lambda-group/scylladb-darwin-arm64') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on macOS: ${arch}`) + return require('./scylladb.linux-riscv64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-riscv64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'ppc64') { + try { + return require('./scylladb.linux-ppc64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-ppc64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 's390x') { + try { + return require('./scylladb.linux-s390x-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('@lambda-group/scylladb-linux-s390x-gnu') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) } - break - case 'freebsd': - if (arch !== 'x64') { - throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } else { + loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) + } +} + +nativeBinding = requireNative() + +if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { + try { + nativeBinding = require('./scylladb.wasi.cjs') + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + loadErrors.push(err) } - localFileExisted = existsSync(join(__dirname, 'scylladb.freebsd-x64.node')) + } + if (!nativeBinding) { try { - if (localFileExisted) { - nativeBinding = require('./scylladb.freebsd-x64.node') - } else { - nativeBinding = require('@lambda-group/scylladb-freebsd-x64') + nativeBinding = require('@lambda-group/scylladb-wasm32-wasi') + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + loadErrors.push(err) } - } catch (e) { - loadError = e } - break - case 'linux': - switch (arch) { - case 'x64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'scylladb.linux-x64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.linux-x64-musl.node') - } else { - nativeBinding = require('@lambda-group/scylladb-linux-x64-musl') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'scylladb.linux-x64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.linux-x64-gnu.node') - } else { - nativeBinding = require('@lambda-group/scylladb-linux-x64-gnu') - } - } catch (e) { - loadError = e - } - } - break - case 'arm64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'scylladb.linux-arm64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.linux-arm64-musl.node') - } else { - nativeBinding = require('@lambda-group/scylladb-linux-arm64-musl') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'scylladb.linux-arm64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.linux-arm64-gnu.node') - } else { - nativeBinding = require('@lambda-group/scylladb-linux-arm64-gnu') - } - } catch (e) { - loadError = e - } - } - break - case 'arm': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'scylladb.linux-arm-musleabihf.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.linux-arm-musleabihf.node') - } else { - nativeBinding = require('@lambda-group/scylladb-linux-arm-musleabihf') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'scylladb.linux-arm-gnueabihf.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.linux-arm-gnueabihf.node') - } else { - nativeBinding = require('@lambda-group/scylladb-linux-arm-gnueabihf') - } - } catch (e) { - loadError = e - } - } - break - case 'riscv64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'scylladb.linux-riscv64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.linux-riscv64-musl.node') - } else { - nativeBinding = require('@lambda-group/scylladb-linux-riscv64-musl') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'scylladb.linux-riscv64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.linux-riscv64-gnu.node') - } else { - nativeBinding = require('@lambda-group/scylladb-linux-riscv64-gnu') - } - } catch (e) { - loadError = e - } - } - break - case 's390x': - localFileExisted = existsSync( - join(__dirname, 'scylladb.linux-s390x-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./scylladb.linux-s390x-gnu.node') - } else { - nativeBinding = require('@lambda-group/scylladb-linux-s390x-gnu') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on Linux: ${arch}`) - } - break - default: - throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) + } } if (!nativeBinding) { - if (loadError) { - throw loadError + if (loadErrors.length > 0) { + // TODO Link to documentation with potential fixes + // - The package owner could build/publish bindings for this arch + // - The user may need to bundle the correct files + // - The user may need to re-install node_modules to get new packages + throw new Error('Failed to load native binding', { cause: loadErrors }) } throw new Error(`Failed to load native binding`) } -const { Compression, Consistency, SerialConsistency, Cluster, VerifyMode, BatchStatement, PreparedStatement, Query, Metrics, ScyllaSession, ScyllaClusterData, Decimal, Duration, Float, List, Map, Set, Uuid, Varint } = nativeBinding - -module.exports.Compression = Compression -module.exports.Consistency = Consistency -module.exports.SerialConsistency = SerialConsistency -module.exports.Cluster = Cluster -module.exports.VerifyMode = VerifyMode -module.exports.BatchStatement = BatchStatement -module.exports.PreparedStatement = PreparedStatement -module.exports.Query = Query -module.exports.Metrics = Metrics -module.exports.ScyllaSession = ScyllaSession -module.exports.ScyllaClusterData = ScyllaClusterData -module.exports.Decimal = Decimal -module.exports.Duration = Duration -module.exports.Float = Float -module.exports.List = List -module.exports.Map = Map -module.exports.Set = Set -module.exports.Uuid = Uuid -module.exports.Varint = Varint +module.exports.BatchStatement = nativeBinding.BatchStatement +module.exports.ScyllaBatchStatement = nativeBinding.ScyllaBatchStatement +module.exports.Cluster = nativeBinding.Cluster +module.exports.ScyllaCluster = nativeBinding.ScyllaCluster +module.exports.Decimal = nativeBinding.Decimal +module.exports.Duration = nativeBinding.Duration +module.exports.Float = nativeBinding.Float +module.exports.List = nativeBinding.List +module.exports.Map = nativeBinding.Map +module.exports.Metrics = nativeBinding.Metrics +module.exports.PreparedStatement = nativeBinding.PreparedStatement +module.exports.Query = nativeBinding.Query +module.exports.ScyllaClusterData = nativeBinding.ScyllaClusterData +module.exports.ScyllaSession = nativeBinding.ScyllaSession +module.exports.Set = nativeBinding.Set +module.exports.Uuid = nativeBinding.Uuid +module.exports.Varint = nativeBinding.Varint +module.exports.Compression = nativeBinding.Compression +module.exports.Consistency = nativeBinding.Consistency +module.exports.SerialConsistency = nativeBinding.SerialConsistency +module.exports.VerifyMode = nativeBinding.VerifyMode const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') -Uuid.prototype[customInspectSymbol] = function () { return this.toString(); } -Duration.prototype[customInspectSymbol] = function () { return this.toString(); } -Decimal.prototype[customInspectSymbol] = function () { return this.toString(); } \ No newline at end of file +nativeBinding.Uuid.prototype[customInspectSymbol] = function () { return this.toString(); } +nativeBinding.Duration.prototype[customInspectSymbol] = function () { return this.toString(); } +nativeBinding.Decimal.prototype[customInspectSymbol] = function () { return this.toString(); } \ No newline at end of file diff --git a/scripts/fix-files.mjs b/scripts/fix-files.mjs index 5f54f0e..17dd5b0 100644 --- a/scripts/fix-files.mjs +++ b/scripts/fix-files.mjs @@ -4,14 +4,17 @@ function addGenericTypes(filename) { const content = readFileSync(filename, "utf8"); const updatedContent = content .replace( - /export class List\b(.*){/, - "export class List$1{", + /export declare class List\b(.*){/, + "export declare class List$1{", ) .replace( - /export class Map\b(.*){/, - "export class Map$1{", + /export declare class Map\b(.*){/, + "export declare class Map$1{", ) - .replace(/export class Set\b(.*){/, "export class Set$1{"); + .replace( + /export declare class Set\b(.*){/, + "export declare class Set$1{", + ); writeFileSync(filename, updatedContent); } @@ -25,9 +28,9 @@ function addInspector(filename) { ` const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') -Uuid.prototype[customInspectSymbol] = function () { return this.toString(); } -Duration.prototype[customInspectSymbol] = function () { return this.toString(); } -Decimal.prototype[customInspectSymbol] = function () { return this.toString(); } +nativeBinding.Uuid.prototype[customInspectSymbol] = function () { return this.toString(); } +nativeBinding.Duration.prototype[customInspectSymbol] = function () { return this.toString(); } +nativeBinding.Decimal.prototype[customInspectSymbol] = function () { return this.toString(); } `, ) .trim(), @@ -43,7 +46,33 @@ function addJSQueryResultType(filename) { type NativeTypes = number | string | Uuid | bigint | Duration | Decimal | Float | List; type WithMapType = NativeTypes | Record | NativeTypes[]; type ParameterWithMapType = WithMapType; -type JSQueryResultType = Record[]; +type JSQueryResult = Record[]; +type TracingReturn = { result: JSQueryResult; tracing: TracingInfo }; + +export interface TracingInfo { + client?: string; // IP address as a string + command?: string; + coordinator?: string; // IP address as a string + duration?: number; + parameters?: Record; + request?: string; + /** + * started_at is a timestamp - time since unix epoch + */ + started_at?: string; + events: TracingEvent[]; +} + +/** + * A single event happening during a traced query + */ +export interface TracingEvent { + event_id: string; + activity?: string; + source?: string; // IP address as a string + source_elapsed?: number; + thread?: string; +} `, ) .trim(), @@ -55,4 +84,4 @@ if (filename.endsWith("index.js")) addInspector(filename); else if (filename.endsWith("index.d.ts")) { addGenericTypes(filename); addJSQueryResultType(filename); -} \ No newline at end of file +} From 60fd4505b7a540dbce133ced16099e4b6d2dd2aa Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Sat, 2 Nov 2024 19:43:23 -0300 Subject: [PATCH 13/13] chore: change lazy variable name Signed-off-by: Daniel Boll --- src/session/scylla_session.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/session/scylla_session.rs b/src/session/scylla_session.rs index 567ba23..e6e135c 100644 --- a/src/session/scylla_session.rs +++ b/src/session/scylla_session.rs @@ -134,7 +134,7 @@ impl ScyllaSession { let should_prepare = options.map_or(false, |options| options.prepare.unwrap_or(false)); - let a = match query { + let result = match query { Either3::A(ref query_str) if should_prepare => { let prepared = self.session.prepare(query_str.clone()).await.map_err(|e| { napi::Error::new( @@ -176,7 +176,7 @@ impl ScyllaSession { r#"Something went wrong with your query."#.to_string(), // TODO: handle different queries here ))?; - match a { + match result { Either::A(results) => Ok(results), Either::B(_tracing) => unreachable!(), } @@ -502,4 +502,4 @@ impl ScyllaSession { .is_some(), ) } -} \ No newline at end of file +}