Skip to content

Commit 662352e

Browse files
authored
feat(scylla): upgrade Scylla driver and add UDT support (#37)
This commit upgrades the Scylla driver to version 0.13.1 and adds support for User Defined Types (UDTs). The `QueryParameter` struct has been updated to handle UDTs and the `QueryResult` struct now parses UDTs correctly. The `execute`, `query`, and `batch` methods in `ScyllaSession` have been updated to handle parameters of UDTs. The `Uuid` struct has been updated to be cloneable and copyable. Signed-off-by: Daniel Boll <danielboll.academico@gmail.com>
1 parent 17c7b7b commit 662352e

File tree

7 files changed

+208
-64
lines changed

7 files changed

+208
-64
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ napi = { version = "2.13.3", default-features = false, features = [
1717
] }
1818
napi-derive = "2.13.0"
1919
tokio = { version = "1", features = ["full"] }
20-
scylla = { version = "0.10.1", features = ["ssl"] }
20+
scylla = { version = "0.13.1", features = [
21+
"ssl",
22+
"full-serialization",
23+
"cloud",
24+
] }
2125
uuid = { version = "1.4.1", features = ["serde", "v4", "fast-rng"] }
2226
serde_json = "1.0"
2327
openssl = { version = "0.10", features = ["vendored"] }

examples/udt.mts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Cluster } from "../index.js";
2+
3+
const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["127.0.0.1:9042"];
4+
5+
console.log(`Connecting to ${nodes}`);
6+
7+
const cluster = new Cluster({ nodes });
8+
const session = await cluster.connect();
9+
10+
await session.execute(
11+
"CREATE KEYSPACE IF NOT EXISTS udt WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }",
12+
);
13+
await session.useKeyspace("udt");
14+
15+
await session.execute(
16+
"CREATE TYPE IF NOT EXISTS address (street text, neighbor text)",
17+
);
18+
await session.execute(
19+
"CREATE TABLE IF NOT EXISTS user (name text, address address, primary key (name))",
20+
);
21+
22+
interface User {
23+
name: string;
24+
address: {
25+
street: string;
26+
neighbor: string;
27+
};
28+
}
29+
30+
const user: User = {
31+
name: "John Doe",
32+
address: {
33+
street: "123 Main St",
34+
neighbor: "Downtown",
35+
},
36+
};
37+
38+
await session.execute("INSERT INTO user (name, address) VALUES (?, ?)", [
39+
user.name,
40+
user.address,
41+
]);
42+
43+
const users = (await session.execute("SELECT * FROM user")) as User[];
44+
console.log(users);

index.d.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export interface ScyllaMaterializedView {
9292
baseTableName: string
9393
}
9494
export type ScyllaCluster = Cluster
95-
export declare class Cluster {
95+
export class Cluster {
9696
/**
9797
* Object config is in the format:
9898
* {
@@ -111,7 +111,7 @@ export type ScyllaBatchStatement = BatchStatement
111111
* These statements can be simple or prepared.
112112
* Only INSERT, UPDATE and DELETE statements are allowed.
113113
*/
114-
export declare class BatchStatement {
114+
export class BatchStatement {
115115
constructor()
116116
/**
117117
* Appends a statement to the batch.
@@ -121,17 +121,17 @@ export declare class BatchStatement {
121121
*/
122122
appendStatement(statement: Query | PreparedStatement): void
123123
}
124-
export declare class PreparedStatement {
124+
export class PreparedStatement {
125125
setConsistency(consistency: Consistency): void
126126
setSerialConsistency(serialConsistency: SerialConsistency): void
127127
}
128-
export declare class Query {
128+
export class Query {
129129
constructor(query: string)
130130
setConsistency(consistency: Consistency): void
131131
setSerialConsistency(serialConsistency: SerialConsistency): void
132132
setPageSize(pageSize: number): void
133133
}
134-
export declare class Metrics {
134+
export class Metrics {
135135
/** Returns counter for nonpaged queries */
136136
getQueriesNum(): bigint
137137
/** Returns counter for pages requested in paged queries */
@@ -151,11 +151,11 @@ export declare class Metrics {
151151
*/
152152
getLatencyPercentileMs(percentile: number): bigint
153153
}
154-
export declare class ScyllaSession {
154+
export class ScyllaSession {
155155
metrics(): Metrics
156156
getClusterData(): Promise<ScyllaClusterData>
157-
execute(query: string | Query | PreparedStatement, parameters?: Array<number | string | Uuid> | undefined | null): Promise<any>
158-
query(scyllaQuery: Query, parameters?: Array<number | string | Uuid> | undefined | null): Promise<any>
157+
execute(query: string | Query | PreparedStatement, parameters?: Array<number | string | Uuid | Record<string, number | string | Uuid>> | undefined | null): Promise<any>
158+
query(scyllaQuery: Query, parameters?: Array<number | string | Uuid | Record<string, number | string | Uuid>> | undefined | null): Promise<any>
159159
prepare(query: string): Promise<PreparedStatement>
160160
/**
161161
* Perform a batch query\
@@ -194,7 +194,7 @@ export declare class ScyllaSession {
194194
* console.log(await session.execute("SELECT * FROM users"));
195195
* ```
196196
*/
197-
batch(batch: BatchStatement, parameters: Array<Array<number | string | Uuid> | undefined | null>): Promise<any>
197+
batch(batch: BatchStatement, parameters: Array<Array<number | string | Uuid | Record<string, number | string | Uuid>> | undefined | null>): Promise<any>
198198
/**
199199
* Sends `USE <keyspace_name>` request on all connections\
200200
* This allows to write `SELECT * FROM table` instead of `SELECT * FROM keyspace.table`\
@@ -264,14 +264,14 @@ export declare class ScyllaSession {
264264
awaitSchemaAgreement(): Promise<Uuid>
265265
checkSchemaAgreement(): Promise<boolean>
266266
}
267-
export declare class ScyllaClusterData {
267+
export class ScyllaClusterData {
268268
/**
269269
* Access keyspaces details collected by the driver Driver collects various schema details like
270270
* tables, partitioners, columns, types. They can be read using this method
271271
*/
272272
getKeyspaceInfo(): Record<string, ScyllaKeyspace> | null
273273
}
274-
export declare class Uuid {
274+
export class Uuid {
275275
/** Generates a random UUID v4. */
276276
static randomV4(): Uuid
277277
/** Parses a UUID from a string. It may fail if the string is not a valid UUID. */

src/helpers/query_parameter.rs

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,90 @@
1+
use std::collections::HashMap;
2+
13
use crate::types::uuid::Uuid;
2-
use napi::bindgen_prelude::Either3;
3-
use scylla::_macro_internal::SerializedValues;
4+
use napi::bindgen_prelude::{Either3, Either4};
5+
use scylla::{
6+
frame::response::result::CqlValue,
7+
serialize::{
8+
row::{RowSerializationContext, SerializeRow},
9+
value::SerializeCql,
10+
RowWriter, SerializationError,
11+
},
12+
};
413

5-
pub struct QueryParameter {
6-
pub(crate) parameters: Option<Vec<Either3<u32, String, Uuid>>>,
14+
pub struct QueryParameter<'a> {
15+
#[allow(clippy::type_complexity)]
16+
pub(crate) parameters:
17+
Option<Vec<Either4<u32, String, &'a Uuid, HashMap<String, Either3<u32, String, &'a Uuid>>>>>,
718
}
819

9-
impl QueryParameter {
10-
pub fn parser(parameters: Option<Vec<Either3<u32, String, &Uuid>>>) -> Option<SerializedValues> {
11-
parameters
12-
.map(|params| {
13-
let mut values = SerializedValues::with_capacity(params.len());
14-
for param in params {
15-
match param {
16-
Either3::A(number) => values.add_value(&(number as i32)).unwrap(),
17-
Either3::B(str) => values.add_value(&str).unwrap(),
18-
Either3::C(uuid) => values.add_value(&(uuid.uuid)).unwrap(),
20+
impl<'a> SerializeRow for QueryParameter<'a> {
21+
fn serialize(
22+
&self,
23+
ctx: &RowSerializationContext<'_>,
24+
writer: &mut RowWriter,
25+
) -> Result<(), SerializationError> {
26+
if let Some(parameters) = &self.parameters {
27+
for (i, parameter) in parameters.iter().enumerate() {
28+
match parameter {
29+
Either4::A(num) => {
30+
CqlValue::Int(*num as i32)
31+
.serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?;
32+
}
33+
Either4::B(str) => {
34+
CqlValue::Text(str.to_string())
35+
.serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?;
36+
}
37+
Either4::C(uuid) => {
38+
CqlValue::Uuid(uuid.get_inner())
39+
.serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?;
40+
}
41+
Either4::D(map) => {
42+
CqlValue::UserDefinedType {
43+
// 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
44+
keyspace: "keyspace".to_string(),
45+
type_name: "type_name".to_string(),
46+
fields: map
47+
.iter()
48+
.map(|(key, value)| match value {
49+
Either3::A(num) => (key.to_string(), Some(CqlValue::Int(*num as i32))),
50+
Either3::B(str) => (key.to_string(), Some(CqlValue::Text(str.to_string()))),
51+
Either3::C(uuid) => (key.to_string(), Some(CqlValue::Uuid(uuid.get_inner()))),
52+
})
53+
.collect::<Vec<(String, Option<CqlValue>)>>(),
54+
}
55+
.serialize(&ctx.columns()[i].typ, writer.make_cell_writer())?;
1956
}
2057
}
21-
values
22-
})
23-
.or(Some(SerializedValues::new()))
58+
}
59+
}
60+
Ok(())
61+
}
62+
63+
fn is_empty(&self) -> bool {
64+
self.parameters.is_none() || self.parameters.as_ref().unwrap().is_empty()
65+
}
66+
}
67+
68+
impl<'a> QueryParameter<'a> {
69+
#[allow(clippy::type_complexity)]
70+
pub fn parser(
71+
parameters: Option<
72+
Vec<Either4<u32, String, &'a Uuid, HashMap<String, Either3<u32, String, &'a Uuid>>>>,
73+
>,
74+
) -> Option<Self> {
75+
if parameters.is_none() {
76+
return Some(QueryParameter { parameters: None });
77+
}
78+
79+
let parameters = parameters.unwrap();
80+
81+
let mut params = Vec::with_capacity(parameters.len());
82+
for parameter in parameters {
83+
params.push(parameter);
84+
}
85+
86+
Some(QueryParameter {
87+
parameters: Some(params),
88+
})
2489
}
2590
}

src/helpers/query_results.rs

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,73 @@
1-
use scylla::frame::response::result::ColumnType;
1+
use scylla::frame::response::result::{ColumnType, CqlValue};
22
pub struct QueryResult {
33
pub(crate) result: scylla::QueryResult,
44
}
55

66
impl QueryResult {
77
pub fn parser(result: scylla::QueryResult) -> serde_json::Value {
8-
if result.result_not_rows().is_ok() {
9-
return serde_json::json!([]);
10-
}
11-
12-
if result.rows.is_none() {
8+
if result.result_not_rows().is_ok() || result.rows.is_none() {
139
return serde_json::json!([]);
1410
}
1511

1612
let rows = result.rows.unwrap();
1713
let column_specs = result.col_specs;
1814

19-
let mut result = serde_json::json!([]);
15+
let mut result_json = serde_json::json!([]);
2016

2117
for row in rows {
2218
let mut row_object = serde_json::Map::new();
2319

2420
for (i, column) in row.columns.iter().enumerate() {
2521
let column_name = column_specs[i].name.clone();
26-
27-
let column_value = match column {
28-
Some(column) => match column_specs[i].typ {
29-
ColumnType::Ascii => serde_json::Value::String(column.as_ascii().unwrap().to_string()),
30-
ColumnType::Text => serde_json::Value::String(column.as_text().unwrap().to_string()),
31-
ColumnType::Uuid => serde_json::Value::String(column.as_uuid().unwrap().to_string()),
32-
ColumnType::Int => serde_json::Value::Number(
33-
serde_json::Number::from_f64(column.as_int().unwrap() as f64).unwrap(),
34-
),
35-
ColumnType::Float => serde_json::Value::Number(
36-
serde_json::Number::from_f64(column.as_float().unwrap() as f64).unwrap(),
37-
),
38-
ColumnType::Timestamp => {
39-
serde_json::Value::String(column.as_date().unwrap().to_string())
40-
}
41-
ColumnType::Date => serde_json::Value::String(column.as_date().unwrap().to_string()),
42-
_ => "Not implemented".into(),
43-
},
44-
None => serde_json::Value::Null,
45-
};
46-
22+
let column_value = Self::parse_value(column, &column_specs[i].typ);
4723
row_object.insert(column_name, column_value);
4824
}
4925

50-
result
26+
result_json
5127
.as_array_mut()
5228
.unwrap()
5329
.push(serde_json::Value::Object(row_object));
5430
}
5531

56-
result
32+
result_json
33+
}
34+
35+
fn parse_value(column: &Option<CqlValue>, column_type: &ColumnType) -> serde_json::Value {
36+
match column {
37+
Some(column) => match column_type {
38+
ColumnType::Ascii => serde_json::Value::String(column.as_ascii().unwrap().to_string()),
39+
ColumnType::Text => serde_json::Value::String(column.as_text().unwrap().to_string()),
40+
ColumnType::Uuid => serde_json::Value::String(column.as_uuid().unwrap().to_string()),
41+
ColumnType::Int => serde_json::Value::Number(
42+
serde_json::Number::from_f64(column.as_int().unwrap() as f64).unwrap(),
43+
),
44+
ColumnType::Float => serde_json::Value::Number(
45+
serde_json::Number::from_f64(column.as_float().unwrap() as f64).unwrap(),
46+
),
47+
ColumnType::Timestamp | ColumnType::Date => {
48+
serde_json::Value::String(column.as_cql_date().unwrap().0.to_string())
49+
}
50+
ColumnType::UserDefinedType { field_types, .. } => {
51+
Self::parse_udt(column.as_udt().unwrap(), field_types)
52+
}
53+
_ => "ColumnType currently not implemented".into(),
54+
},
55+
None => serde_json::Value::Null,
56+
}
57+
}
58+
59+
fn parse_udt(
60+
udt: &[(String, Option<CqlValue>)],
61+
field_types: &[(String, ColumnType)],
62+
) -> serde_json::Value {
63+
let mut result = serde_json::Map::new();
64+
65+
for (i, (field_name, field_value)) in udt.iter().enumerate() {
66+
let field_type = &field_types[i].1;
67+
let parsed_value = Self::parse_value(field_value, field_type);
68+
result.insert(field_name.clone(), parsed_value);
69+
}
70+
71+
serde_json::Value::Object(result)
5772
}
5873
}

0 commit comments

Comments
 (0)