use crate::schema::CqlType;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlStatement {
Select(CqlSelect),
Insert(CqlInsert),
Update(CqlUpdate),
Delete(CqlDelete),
CreateTable(CqlCreateTable),
DropTable(CqlDropTable),
CreateIndex(CqlCreateIndex),
AlterTable(CqlAlterTable),
CreateType(CqlCreateType),
DropType(CqlDropType),
Use(CqlUse),
Truncate(CqlTruncate),
Batch(CqlBatch),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlSelect {
pub distinct: bool,
pub select_list: Vec<CqlSelectItem>,
pub from: CqlTable,
pub where_clause: Option<CqlExpression>,
pub order_by: Option<Vec<CqlOrderBy>>,
pub limit: Option<u64>,
pub allow_filtering: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlSelectItem {
Wildcard,
Expression {
expression: CqlExpression,
alias: Option<CqlIdentifier>,
},
Function {
name: CqlIdentifier,
args: Vec<CqlExpression>,
alias: Option<CqlIdentifier>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlInsert {
pub table: CqlTable,
pub columns: Vec<CqlIdentifier>,
pub values: CqlInsertValues,
pub if_not_exists: bool,
pub using: Option<CqlUsing>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlInsertValues {
Values(Vec<CqlExpression>),
Json(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlUpdate {
pub table: CqlTable,
pub using: Option<CqlUsing>,
pub assignments: Vec<CqlAssignment>,
pub where_clause: CqlExpression,
pub if_condition: Option<CqlExpression>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlAssignment {
pub column: CqlIdentifier,
pub operator: CqlAssignmentOperator,
pub value: CqlExpression,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlAssignmentOperator {
Assign,
AddAssign,
SubAssign,
ListAppend,
ListPrepend,
SetAdd,
SetRemove,
MapUpdate(CqlExpression),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlDelete {
pub columns: Vec<CqlIdentifier>,
pub table: CqlTable,
pub using: Option<CqlUsing>,
pub where_clause: CqlExpression,
pub if_condition: Option<CqlExpression>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlCreateTable {
pub if_not_exists: bool,
pub table: CqlTable,
pub columns: Vec<CqlColumnDef>,
pub primary_key: CqlPrimaryKey,
pub options: CqlTableOptions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlColumnDef {
pub name: CqlIdentifier,
pub data_type: CqlDataType,
pub is_static: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlPrimaryKey {
pub partition_key: Vec<CqlIdentifier>,
pub clustering_key: Vec<CqlIdentifier>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlDropTable {
pub if_exists: bool,
pub table: CqlTable,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlCreateIndex {
pub if_not_exists: bool,
pub name: Option<CqlIdentifier>,
pub table: CqlTable,
pub columns: Vec<CqlIndexColumn>,
pub using: Option<String>,
pub options: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlIndexColumn {
Column(CqlIdentifier),
Keys(CqlIdentifier),
Values(CqlIdentifier),
Entries(CqlIdentifier),
Full(CqlIdentifier),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlAlterTable {
pub table: CqlTable,
pub operation: CqlAlterTableOp,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlAlterTableOp {
AddColumn(CqlColumnDef),
DropColumn(CqlIdentifier),
AlterColumn {
column: CqlIdentifier,
new_type: CqlDataType,
},
RenameColumn {
old_name: CqlIdentifier,
new_name: CqlIdentifier,
},
WithOptions(CqlTableOptions),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlCreateType {
pub if_not_exists: bool,
pub name: CqlIdentifier,
pub fields: Vec<CqlUdtField>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlUdtField {
pub name: CqlIdentifier,
pub data_type: CqlDataType,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlDropType {
pub if_exists: bool,
pub name: CqlIdentifier,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlUse {
pub keyspace: CqlIdentifier,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlTruncate {
pub table: CqlTable,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlBatch {
pub batch_type: CqlBatchType,
pub using: Option<CqlUsing>,
pub statements: Vec<CqlBatchStatement>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlBatchType {
Logged,
Unlogged,
Counter,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlBatchStatement {
Insert(CqlInsert),
Update(CqlUpdate),
Delete(CqlDelete),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlExpression {
Literal(CqlLiteral),
Column(CqlIdentifier),
Parameter(u32),
NamedParameter(String),
Binary {
left: Box<CqlExpression>,
operator: CqlBinaryOperator,
right: Box<CqlExpression>,
},
Unary {
operator: CqlUnaryOperator,
operand: Box<CqlExpression>,
},
Function {
name: CqlIdentifier,
args: Vec<CqlExpression>,
},
In {
expression: Box<CqlExpression>,
values: Vec<CqlExpression>,
},
Contains {
column: CqlIdentifier,
value: Box<CqlExpression>,
},
ContainsKey {
column: CqlIdentifier,
key: Box<CqlExpression>,
},
CollectionAccess {
collection: Box<CqlExpression>,
index: Box<CqlExpression>,
},
FieldAccess {
object: Box<CqlExpression>,
field: CqlIdentifier,
},
Case {
when_clauses: Vec<CqlWhenClause>,
else_clause: Option<Box<CqlExpression>>,
},
Cast {
expression: Box<CqlExpression>,
target_type: CqlDataType,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlWhenClause {
pub condition: CqlExpression,
pub result: CqlExpression,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlBinaryOperator {
And,
Or,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
Add,
Sub,
Mul,
Div,
Mod,
Like,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlUnaryOperator {
Not,
Minus,
Plus,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlLiteral {
Null,
Boolean(bool),
Integer(i64),
Float(f64),
String(String),
Uuid(String),
Blob(String),
Collection(CqlCollectionLiteral),
Udt(CqlUdtLiteral),
Tuple(Vec<CqlLiteral>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlCollectionLiteral {
List(Vec<CqlLiteral>),
Set(Vec<CqlLiteral>),
Map(Vec<(CqlLiteral, CqlLiteral)>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlUdtLiteral {
pub type_name: Option<CqlIdentifier>,
pub fields: Vec<(CqlIdentifier, CqlLiteral)>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlDataType {
Boolean,
TinyInt,
SmallInt,
Int,
BigInt,
Varint,
Decimal,
Float,
Double,
Text,
Ascii,
Varchar,
Blob,
Timestamp,
Date,
Time,
Uuid,
TimeUuid,
Inet,
Duration,
Counter,
List(Box<CqlDataType>),
Set(Box<CqlDataType>),
Map(Box<CqlDataType>, Box<CqlDataType>),
Tuple(Vec<CqlDataType>),
Udt(CqlIdentifier),
Frozen(Box<CqlDataType>),
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CqlIdentifier {
pub name: String,
pub quoted: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlTable {
pub keyspace: Option<CqlIdentifier>,
pub name: CqlIdentifier,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlOrderBy {
pub column: CqlIdentifier,
pub direction: CqlSortDirection,
}
pub type CqlOrdering = CqlOrderBy;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlLimit {
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlTtl {
pub seconds: Option<CqlExpression>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlTimestamp {
pub microseconds: Option<CqlExpression>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CqlSortDirection {
Asc,
Desc,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CqlUsing {
pub ttl: Option<CqlExpression>,
pub timestamp: Option<CqlExpression>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct CqlTableOptions {
pub options: HashMap<String, CqlLiteral>,
}
impl CqlIdentifier {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
quoted: false,
}
}
pub fn quoted(name: impl Into<String>) -> Self {
Self {
name: name.into(),
quoted: true,
}
}
pub fn as_str(&self) -> &str {
&self.name
}
pub fn name(&self) -> &str {
&self.name
}
pub fn is_quoted(&self) -> bool {
self.quoted
}
pub fn needs_quoting(&self) -> bool {
self.quoted || !self.is_valid_unquoted()
}
fn is_valid_unquoted(&self) -> bool {
let mut chars = self.name.chars();
let Some(first) = chars.next() else {
return false;
};
(first.is_ascii_alphabetic() || first == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
}
impl CqlTable {
pub fn new(name: impl Into<String>) -> Self {
Self {
keyspace: None,
name: CqlIdentifier::new(name),
}
}
pub fn with_keyspace(keyspace: impl Into<String>, name: impl Into<String>) -> Self {
Self {
keyspace: Some(CqlIdentifier::new(keyspace)),
name: CqlIdentifier::new(name),
}
}
pub fn full_name(&self) -> String {
match &self.keyspace {
Some(ks) => format!("{}.{}", ks.as_str(), self.name.as_str()),
None => self.name.as_str().to_string(),
}
}
pub fn name(&self) -> &CqlIdentifier {
&self.name
}
pub fn keyspace(&self) -> Option<&CqlIdentifier> {
self.keyspace.as_ref()
}
}
impl From<CqlDataType> for CqlType {
fn from(data_type: CqlDataType) -> Self {
match data_type {
CqlDataType::Boolean => CqlType::Boolean,
CqlDataType::TinyInt => CqlType::TinyInt,
CqlDataType::SmallInt => CqlType::SmallInt,
CqlDataType::Int => CqlType::Int,
CqlDataType::BigInt => CqlType::BigInt,
CqlDataType::Float => CqlType::Float,
CqlDataType::Double => CqlType::Double,
CqlDataType::Decimal => CqlType::Decimal,
CqlDataType::Text | CqlDataType::Varchar => CqlType::Text,
CqlDataType::Ascii => CqlType::Ascii,
CqlDataType::Blob => CqlType::Blob,
CqlDataType::Timestamp => CqlType::Timestamp,
CqlDataType::Date => CqlType::Date,
CqlDataType::Time => CqlType::Time,
CqlDataType::Uuid => CqlType::Uuid,
CqlDataType::TimeUuid => CqlType::TimeUuid,
CqlDataType::Inet => CqlType::Inet,
CqlDataType::Duration => CqlType::Duration,
CqlDataType::List(inner) => CqlType::List(Box::new((*inner).into())),
CqlDataType::Set(inner) => CqlType::Set(Box::new((*inner).into())),
CqlDataType::Map(key, value) => {
CqlType::Map(Box::new((*key).into()), Box::new((*value).into()))
}
CqlDataType::Tuple(types) => {
CqlType::Tuple(types.into_iter().map(|t| t.into()).collect())
}
CqlDataType::Udt(name) => CqlType::Udt(name.as_str().to_string(), vec![]),
CqlDataType::Frozen(inner) => CqlType::Frozen(Box::new((*inner).into())),
CqlDataType::Custom(name) => CqlType::Custom(name),
CqlDataType::Varint => CqlType::BigInt, CqlDataType::Counter => CqlType::Counter,
}
}
}
impl From<CqlType> for CqlDataType {
fn from(cql_type: CqlType) -> Self {
match cql_type {
CqlType::Boolean => CqlDataType::Boolean,
CqlType::TinyInt => CqlDataType::TinyInt,
CqlType::SmallInt => CqlDataType::SmallInt,
CqlType::Int => CqlDataType::Int,
CqlType::BigInt => CqlDataType::BigInt,
CqlType::Counter => CqlDataType::Counter,
CqlType::Float => CqlDataType::Float,
CqlType::Double => CqlDataType::Double,
CqlType::Decimal => CqlDataType::Decimal,
CqlType::Text | CqlType::Varchar => CqlDataType::Text,
CqlType::Ascii => CqlDataType::Ascii,
CqlType::Blob => CqlDataType::Blob,
CqlType::Timestamp => CqlDataType::Timestamp,
CqlType::Date => CqlDataType::Date,
CqlType::Time => CqlDataType::Time,
CqlType::Uuid => CqlDataType::Uuid,
CqlType::TimeUuid => CqlDataType::TimeUuid,
CqlType::Inet => CqlDataType::Inet,
CqlType::Duration => CqlDataType::Duration,
CqlType::Varint => CqlDataType::Custom("varint".to_string()),
CqlType::List(inner) => CqlDataType::List(Box::new((*inner).into())),
CqlType::Set(inner) => CqlDataType::Set(Box::new((*inner).into())),
CqlType::Map(key, value) => {
CqlDataType::Map(Box::new((*key).into()), Box::new((*value).into()))
}
CqlType::Tuple(types) => {
CqlDataType::Tuple(types.into_iter().map(|t| t.into()).collect())
}
CqlType::Udt(name, _) => CqlDataType::Udt(CqlIdentifier::new(name)),
CqlType::Frozen(inner) => CqlDataType::Frozen(Box::new((*inner).into())),
CqlType::Custom(name) => CqlDataType::Custom(name),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identifier_creation() {
let id1 = CqlIdentifier::new("test");
assert_eq!(id1.name, "test");
assert!(!id1.quoted);
assert!(!id1.needs_quoting());
let id2 = CqlIdentifier::quoted("test");
assert_eq!(id2.name, "test");
assert!(id2.quoted);
assert!(id2.needs_quoting());
}
#[test]
fn test_identifier_validation() {
assert!(CqlIdentifier::new("valid_name").is_valid_unquoted());
assert!(CqlIdentifier::new("_valid").is_valid_unquoted());
assert!(CqlIdentifier::new("valid123").is_valid_unquoted());
assert!(!CqlIdentifier::new("123invalid").is_valid_unquoted());
assert!(!CqlIdentifier::new("invalid-name").is_valid_unquoted());
assert!(!CqlIdentifier::new("").is_valid_unquoted());
}
#[test]
fn test_table_creation() {
let table1 = CqlTable::new("users");
assert_eq!(table1.name.as_str(), "users");
assert!(table1.keyspace.is_none());
assert_eq!(table1.full_name(), "users");
let table2 = CqlTable::with_keyspace("test", "users");
assert_eq!(table2.keyspace.as_ref().unwrap().as_str(), "test");
assert_eq!(table2.name.as_str(), "users");
assert_eq!(table2.full_name(), "test.users");
}
#[test]
fn test_data_type_conversion() {
let cql_type = CqlType::List(Box::new(CqlType::Text));
let data_type: CqlDataType = cql_type.clone().into();
let back_to_cql: CqlType = data_type.into();
assert_eq!(cql_type, back_to_cql);
}
#[test]
fn test_identifier_needs_quoting_rules() {
let numeric_start = CqlIdentifier::new("123abc");
assert!(numeric_start.needs_quoting());
let mixed_case = CqlIdentifier::new("CamelCase");
assert!(!mixed_case.needs_quoting());
let quoted = CqlIdentifier::quoted("any value");
assert!(quoted.needs_quoting());
}
#[test]
fn test_table_options_default_is_empty() {
let options = CqlTableOptions::default();
assert!(options.options.is_empty());
}
#[test]
fn test_varint_and_counter_mapping() {
let big_int = CqlType::from(CqlDataType::Varint);
assert_eq!(big_int, CqlType::BigInt);
let counter_type = CqlType::from(CqlDataType::Counter);
assert_eq!(counter_type, CqlType::Counter);
}
}