use super::ast::*;
use super::error::ParserError;
use super::traits::SourcePosition;
use crate::error::Result;
use nom::{
branch::alt,
bytes::complete::{tag_no_case, take_while1, take_while_m_n},
character::complete::{char, digit1, multispace0, multispace1},
combinator::{map, opt, recognize},
multi::{separated_list0, separated_list1},
sequence::{preceded, separated_pair, tuple},
IResult,
};
const MAX_NESTING_DEPTH: usize = 32;
const MAX_COLLECTION_SIZE: usize = 65536;
const MAX_INPUT_LENGTH: usize = 16 * 1024 * 1024;
const MAX_IDENTIFIER_LENGTH: usize = 48;
const MAX_BATCH_STATEMENTS: usize = 65535;
fn starts_with_ascii_ci(input: &str, keyword: &str) -> bool {
let bytes = input.as_bytes();
let kw = keyword.as_bytes();
bytes.len() >= kw.len()
&& bytes[..kw.len()]
.iter()
.zip(kw)
.all(|(a, b)| a.eq_ignore_ascii_case(b))
}
fn check_input_length(input: &str) -> Result<()> {
if input.len() > MAX_INPUT_LENGTH {
return Err(ParserError::resource_limit(
"input_length",
MAX_INPUT_LENGTH as u64,
input.len() as u64,
)
.into());
}
Ok(())
}
fn finish_parse<T>(kind: &str, result: IResult<&str, T>) -> Result<T> {
match result {
Ok((_, value)) => Ok(value),
Err(e) => Err(ParserError::syntax(
format!("Failed to parse {} statement: {:?}", kind, e),
SourcePosition::start(),
)
.into()),
}
}
fn validate_identifier(name: &str) -> Result<()> {
if name.is_empty() {
return Err(
ParserError::lexical("Identifier cannot be empty", SourcePosition::start()).into(),
);
}
if name.len() > MAX_IDENTIFIER_LENGTH {
return Err(ParserError::resource_limit(
"identifier_length",
MAX_IDENTIFIER_LENGTH as u64,
name.len() as u64,
)
.into());
}
if name.chars().any(|c| c.is_ascii_control()) {
return Err(ParserError::lexical(
"Identifier contains control characters",
SourcePosition::start(),
)
.into());
}
if name.contains('\0') {
return Err(ParserError::lexical(
"Identifier contains null bytes",
SourcePosition::start(),
)
.into());
}
Ok(())
}
#[allow(dead_code)]
fn sanitize_for_filesystem(identifier: &str) -> String {
identifier
.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
'\0' => '_',
c if c.is_ascii_control() => '_',
c => c,
})
.take(MAX_IDENTIFIER_LENGTH)
.collect()
}
fn keyword(s: &str) -> impl Fn(&str) -> IResult<&str, &str> + '_ {
move |input| tag_no_case(s)(input)
}
fn ws(input: &str) -> IResult<&str, &str> {
multispace0(input)
}
fn ws1(input: &str) -> IResult<&str, &str> {
multispace1(input)
}
fn parse_quoted_identifier(input: &str) -> IResult<&str, String> {
let (input, _) = char('"')(input)?;
let mut result = String::new();
let mut chars = input.chars();
let mut consumed = 0;
loop {
match chars.next() {
Some('"') => {
if chars.clone().next() == Some('"') {
result.push('"');
chars.next();
consumed += 2;
} else {
consumed += 1;
break;
}
}
Some(c) => {
result.push(c);
consumed += c.len_utf8();
}
None => {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Escaped,
)))
}
}
}
Ok((&input[consumed..], result))
}
fn identifier(input: &str) -> IResult<&str, CqlIdentifier> {
let is_quoted = input.starts_with('"');
let (remaining, name_str) = if is_quoted {
parse_quoted_identifier(input)?
} else {
let (rem, n) = take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)?;
(rem, n.to_string())
};
if let Err(_e) = validate_identifier(&name_str) {
return Err(nom::Err::Failure(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
Ok((
remaining,
CqlIdentifier {
name: name_str,
quoted: is_quoted,
},
))
}
fn qualified_table_name(input: &str) -> IResult<&str, CqlTable> {
let (input, first) = identifier(input)?;
let (input, second) = opt(preceded(char('.'), identifier))(input)?;
match second {
Some(table) => Ok((
input,
CqlTable {
keyspace: Some(first),
name: table,
},
)),
None => Ok((
input,
CqlTable {
keyspace: None,
name: first,
},
)),
}
}
fn integer_literal(input: &str) -> IResult<&str, i64> {
let (rest, num_str) = recognize(tuple((opt(char('-')), digit1)))(input)?;
let value = num_str.parse::<i64>().map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
})?;
Ok((rest, value))
}
fn float_literal(input: &str) -> IResult<&str, f64> {
let (rest, num_str) = recognize(tuple((opt(char('-')), digit1, char('.'), digit1)))(input)?;
let value = num_str.parse::<f64>().map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
})?;
Ok((rest, value))
}
fn string_literal(input: &str) -> IResult<&str, String> {
let (input, _) = char('\'')(input)?;
let mut result = String::new();
let mut chars = input.chars();
let mut consumed = 0;
loop {
match chars.next() {
Some('\'') => {
if chars.clone().next() == Some('\'') {
result.push('\'');
chars.next();
consumed += 2;
} else {
consumed += 1;
break;
}
}
Some('\\') => {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('\\') => result.push('\\'),
Some('\'') => result.push('\''),
Some(c) => result.push(c),
None => {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Escaped,
)))
}
}
consumed += 2;
}
Some(c) => {
result.push(c);
consumed += c.len_utf8();
}
None => {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Escaped,
)))
}
}
}
Ok((&input[consumed..], result))
}
fn uuid_literal(input: &str) -> IResult<&str, String> {
fn hex_run(n: usize) -> impl Fn(&str) -> IResult<&str, &str> {
move |i| take_while_m_n(n, n, |c: char| c.is_ascii_hexdigit())(i)
}
let (rest, matched) = recognize(tuple((
hex_run(8),
char('-'),
hex_run(4),
char('-'),
hex_run(4),
char('-'),
hex_run(4),
char('-'),
hex_run(12),
)))(input)?;
Ok((rest, matched.to_string()))
}
fn blob_literal(input: &str) -> IResult<&str, String> {
let (input, _) = tag_no_case("0x")(input)?;
let (input, hex) = take_while1(|c: char| c.is_ascii_hexdigit())(input)?;
if hex.len() % 2 != 0 {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
Ok((input, hex.to_string()))
}
fn too_large<T>(input: &str) -> IResult<&str, T> {
Err(nom::Err::Failure(nom::error::Error::new(
input,
nom::error::ErrorKind::TooLarge,
)))
}
fn collection_body<'a, T, P>(
input: &'a str,
depth: usize,
open: char,
close: char,
mut parse_items: P,
) -> IResult<&'a str, Vec<T>>
where
P: FnMut(&'a str, usize) -> IResult<&'a str, Vec<T>>,
{
if depth >= MAX_NESTING_DEPTH {
return too_large(input);
}
let (input, _) = char(open)(input)?;
let (input, _) = ws(input)?;
let (input, items) = parse_items(input, depth)?;
let (input, _) = ws(input)?;
let (input, _) = char(close)(input)?;
if items.len() > MAX_COLLECTION_SIZE {
return too_large(input);
}
Ok((input, items))
}
fn list_literal_depth(input: &str, depth: usize) -> IResult<&str, CqlCollectionLiteral> {
let (input, items) = collection_body(input, depth, '[', ']', |i, d| {
separated_list0(tuple((ws, char(','), ws)), |i2| literal_depth(i2, d + 1))(i)
})?;
Ok((input, CqlCollectionLiteral::List(items)))
}
fn set_literal_depth(input: &str, depth: usize) -> IResult<&str, CqlCollectionLiteral> {
let (input, items) = collection_body(input, depth, '{', '}', |i, d| {
separated_list0(tuple((ws, char(','), ws)), |i2| literal_depth(i2, d + 1))(i)
})?;
Ok((input, CqlCollectionLiteral::Set(items)))
}
fn map_literal_depth(input: &str, depth: usize) -> IResult<&str, CqlCollectionLiteral> {
let (input, pairs) = collection_body(input, depth, '{', '}', |i, d| {
separated_list0(
tuple((ws, char(','), ws)),
separated_pair(
|i2| literal_depth(i2, d + 1),
tuple((ws, char(':'), ws)),
|i2| literal_depth(i2, d + 1),
),
)(i)
})?;
Ok((input, CqlCollectionLiteral::Map(pairs)))
}
fn literal_depth(input: &str, depth: usize) -> IResult<&str, CqlLiteral> {
alt((
map(keyword("null"), |_| CqlLiteral::Null),
map(keyword("true"), |_| CqlLiteral::Boolean(true)),
map(keyword("false"), |_| CqlLiteral::Boolean(false)),
map(string_literal, CqlLiteral::String),
map(blob_literal, CqlLiteral::Blob),
map(uuid_literal, CqlLiteral::Uuid),
map(float_literal, CqlLiteral::Float),
map(integer_literal, CqlLiteral::Integer),
map(|i| list_literal_depth(i, depth), CqlLiteral::Collection),
map(|i| set_literal_depth(i, depth), CqlLiteral::Collection),
map(|i| map_literal_depth(i, depth), CqlLiteral::Collection),
))(input)
}
fn literal(input: &str) -> IResult<&str, CqlLiteral> {
literal_depth(input, 0)
}
fn expression(input: &str) -> IResult<&str, CqlExpression> {
alt((
map(char('?'), |_| CqlExpression::Parameter(0)),
map(preceded(char(':'), identifier), |id| {
CqlExpression::NamedParameter(id.name)
}),
map(literal, CqlExpression::Literal),
map(identifier, CqlExpression::Column),
))(input)
}
fn where_clause(input: &str) -> IResult<&str, CqlExpression> {
let (input, _) = ws(input)?;
let (input, _) = keyword("where")(input)?;
let (input, _) = ws1(input)?;
let (input, conditions) =
separated_list1(tuple((ws, keyword("and"), ws)), where_condition)(input)?;
let result = conditions
.into_iter()
.reduce(|acc, cond| CqlExpression::Binary {
left: Box::new(acc),
operator: CqlBinaryOperator::And,
right: Box::new(cond),
})
.ok_or_else(|| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Many1))
})?;
Ok((input, result))
}
fn where_condition(input: &str) -> IResult<&str, CqlExpression> {
let (input, left) = identifier(input)?;
let (input, _) = ws(input)?;
let (input, op) = comparison_operator(input)?;
let (input, _) = ws(input)?;
let (input, right) = expression(input)?;
Ok((
input,
CqlExpression::Binary {
left: Box::new(CqlExpression::Column(left)),
operator: op,
right: Box::new(right),
},
))
}
fn comparison_operator(input: &str) -> IResult<&str, CqlBinaryOperator> {
alt((
map(char('='), |_| CqlBinaryOperator::Eq),
map(tag_no_case("!="), |_| CqlBinaryOperator::Ne),
map(tag_no_case("<="), |_| CqlBinaryOperator::Le),
map(tag_no_case(">="), |_| CqlBinaryOperator::Ge),
map(char('<'), |_| CqlBinaryOperator::Lt),
map(char('>'), |_| CqlBinaryOperator::Gt),
))(input)
}
fn using_clause(input: &str) -> IResult<&str, CqlUsing> {
let (input, _) = ws(input)?;
let (input, _) = keyword("using")(input)?;
let (input, _) = ws1(input)?;
let (input, first_option) = using_option(input)?;
let (input, second_option) =
opt(preceded(tuple((ws, keyword("and"), ws)), using_option))(input)?;
let mut ttl = None;
let mut timestamp = None;
for opt_val in [Some(first_option), second_option].into_iter().flatten() {
match opt_val {
UsingOption::Ttl(t) => ttl = Some(t),
UsingOption::Timestamp(ts) => timestamp = Some(ts),
}
}
Ok((input, CqlUsing { ttl, timestamp }))
}
enum UsingOption {
Ttl(CqlExpression),
Timestamp(CqlExpression),
}
fn using_option(input: &str) -> IResult<&str, UsingOption> {
alt((
map(
preceded(tuple((keyword("ttl"), ws)), expression),
UsingOption::Ttl,
),
map(
preceded(tuple((keyword("timestamp"), ws)), expression),
UsingOption::Timestamp,
),
))(input)
}
pub fn parse_insert_statement(input: &str) -> Result<CqlInsert> {
check_input_length(input)?;
finish_parse("INSERT", insert_statement_impl(input))
}
fn insert_trailer(input: &str) -> IResult<&str, (bool, Option<CqlUsing>)> {
let (input, _) = ws(input)?;
let (input, if_not_exists) = opt(tuple((
keyword("if"),
ws1,
keyword("not"),
ws1,
keyword("exists"),
)))(input)?;
let (input, using) = opt(using_clause)(input)?;
let (input, _) = ws(input)?;
Ok((input, (if_not_exists.is_some(), using)))
}
fn insert_statement_impl(input: &str) -> IResult<&str, CqlInsert> {
let (input, _) = ws(input)?;
let (input, _) = keyword("insert")(input)?;
let (input, _) = ws1(input)?;
let (input, _) = keyword("into")(input)?;
let (input, _) = ws1(input)?;
let (input, table) = qualified_table_name(input)?;
let (input, _) = ws(input)?;
if let Ok((json_input, _)) = keyword("json")(input) {
let (json_input, _) = ws1(json_input)?;
let (json_input, json_str) = string_literal(json_input)?;
let (json_input, (if_not_exists, using)) = insert_trailer(json_input)?;
return Ok((
json_input,
CqlInsert {
table,
columns: vec![],
values: CqlInsertValues::Json(json_str),
if_not_exists,
using,
},
));
}
let (input, _) = char('(')(input)?;
let (input, _) = ws(input)?;
let (input, columns) = separated_list1(tuple((ws, char(','), ws)), identifier)(input)?;
let (input, _) = ws(input)?;
let (input, _) = char(')')(input)?;
let (input, _) = ws(input)?;
let (input, _) = keyword("values")(input)?;
let (input, _) = ws(input)?;
let (input, _) = char('(')(input)?;
let (input, _) = ws(input)?;
let (input, values) = separated_list1(tuple((ws, char(','), ws)), expression)(input)?;
let (input, _) = ws(input)?;
let (input, _) = char(')')(input)?;
let (input, (if_not_exists, using)) = insert_trailer(input)?;
Ok((
input,
CqlInsert {
table,
columns,
values: CqlInsertValues::Values(values),
if_not_exists,
using,
},
))
}
pub fn parse_update_statement(input: &str) -> Result<CqlUpdate> {
check_input_length(input)?;
finish_parse("UPDATE", update_statement_impl(input))
}
fn update_statement_impl(input: &str) -> IResult<&str, CqlUpdate> {
let (input, _) = ws(input)?;
let (input, _) = keyword("update")(input)?;
let (input, _) = ws1(input)?;
let (input, table) = qualified_table_name(input)?;
let (input, _) = ws(input)?;
let (input, using) = opt(using_clause)(input)?;
let (input, _) = ws(input)?;
let (input, _) = keyword("set")(input)?;
let (input, _) = ws1(input)?;
let (input, assignments) = separated_list1(tuple((ws, char(','), ws)), assignment)(input)?;
let (input, _) = ws(input)?;
let (input, where_expr) = where_clause(input)?;
let (input, _) = ws(input)?;
let (input, if_condition) = opt(preceded(tuple((keyword("if"), ws1)), where_condition))(input)?;
let (input, _) = ws(input)?;
Ok((
input,
CqlUpdate {
table,
using,
assignments,
where_clause: where_expr,
if_condition,
},
))
}
fn assignment(input: &str) -> IResult<&str, CqlAssignment> {
let (input, column) = identifier(input)?;
let (input, _) = ws(input)?;
let (input, operator) = assignment_operator(input)?;
let (input, _) = ws(input)?;
let (input, value) = expression(input)?;
Ok((
input,
CqlAssignment {
column,
operator,
value,
},
))
}
fn assignment_operator(input: &str) -> IResult<&str, CqlAssignmentOperator> {
alt((
map(tag_no_case("+="), |_| CqlAssignmentOperator::AddAssign),
map(tag_no_case("-="), |_| CqlAssignmentOperator::SubAssign),
map(char('='), |_| CqlAssignmentOperator::Assign),
))(input)
}
pub fn parse_delete_statement(input: &str) -> Result<CqlDelete> {
check_input_length(input)?;
finish_parse("DELETE", delete_statement_impl(input))
}
fn delete_statement_impl(input: &str) -> IResult<&str, CqlDelete> {
let (input, _) = ws(input)?;
let (input, _) = keyword("delete")(input)?;
let (input, _) = ws(input)?;
let trimmed = input.trim_start();
let has_from = starts_with_ascii_ci(trimmed, "from");
let (input, columns) = if has_from {
(input, vec![])
} else {
let (input, cols) = separated_list1(tuple((ws, char(','), ws)), identifier)(input)?;
let (input, _) = ws(input)?;
(input, cols)
};
let (input, _) = keyword("from")(input)?;
let (input, _) = ws1(input)?;
let (input, table) = qualified_table_name(input)?;
let (input, _) = ws(input)?;
let (input, using) = opt(using_clause)(input)?;
let (input, _) = ws(input)?;
let (input, where_expr) = where_clause(input)?;
let (input, _) = ws(input)?;
let (input, if_condition) = opt(preceded(tuple((keyword("if"), ws1)), where_condition))(input)?;
let (input, _) = ws(input)?;
Ok((
input,
CqlDelete {
columns,
table,
using,
where_clause: where_expr,
if_condition,
},
))
}
pub fn parse_batch_statement(input: &str) -> Result<CqlBatch> {
check_input_length(input)?;
finish_parse("BATCH", batch_statement_impl(input))
}
fn batch_statement_impl(input: &str) -> IResult<&str, CqlBatch> {
let (input, _) = ws(input)?;
let (input, _) = keyword("begin")(input)?;
let (input, _) = ws1(input)?;
let (input, batch_type) = batch_type_parser(input)?;
let (input, _) = keyword("batch")(input)?;
let (input, _) = ws(input)?;
let (input, using) = opt(using_clause)(input)?;
let (input, _) = ws(input)?;
let mut statements = Vec::new();
let mut remaining = input;
loop {
let trimmed = remaining.trim_start();
if starts_with_ascii_ci(trimmed, "apply") {
remaining = trimmed;
break;
}
if trimmed.is_empty() {
break;
}
let (rest, stmt) = if starts_with_ascii_ci(trimmed, "insert") {
let (r, ins) = insert_statement_impl(trimmed)?;
(r, CqlBatchStatement::Insert(ins))
} else if starts_with_ascii_ci(trimmed, "update") {
let (r, upd) = update_statement_impl(trimmed)?;
(r, CqlBatchStatement::Update(upd))
} else if starts_with_ascii_ci(trimmed, "delete") {
let (r, del) = delete_statement_impl(trimmed)?;
(r, CqlBatchStatement::Delete(del))
} else {
return Err(nom::Err::Failure(nom::error::Error::new(
trimmed,
nom::error::ErrorKind::Tag,
)));
};
if statements.len() >= MAX_BATCH_STATEMENTS {
return Err(nom::Err::Failure(nom::error::Error::new(
trimmed,
nom::error::ErrorKind::TooLarge,
)));
}
statements.push(stmt);
let rest = rest.trim_start();
remaining = rest.strip_prefix(';').unwrap_or(rest);
}
let (input, _) = keyword("apply")(remaining)?;
let (input, _) = ws1(input)?;
let (input, _) = keyword("batch")(input)?;
let (input, _) = ws(input)?;
let input = input.strip_prefix(';').unwrap_or(input);
let (input, _) = ws(input)?;
Ok((
input,
CqlBatch {
batch_type,
using,
statements,
},
))
}
fn batch_type_parser(input: &str) -> IResult<&str, CqlBatchType> {
let trimmed = input.trim_start();
for (kw, ty) in [
("unlogged", CqlBatchType::Unlogged),
("counter", CqlBatchType::Counter),
("logged", CqlBatchType::Logged),
] {
if starts_with_ascii_ci(trimmed, kw) {
let (rest, _) = keyword(kw)(trimmed)?;
let (rest, _) = ws1(rest)?;
return Ok((rest, ty));
}
}
Ok((input, CqlBatchType::Logged))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_insert() {
let cql = "INSERT INTO users (id, name) VALUES (?, ?)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert_eq!(insert.table.name.name, "users");
assert_eq!(insert.columns.len(), 2);
assert_eq!(insert.columns[0].name, "id");
assert_eq!(insert.columns[1].name, "name");
}
#[test]
fn test_parse_insert_with_literals() {
let cql = "INSERT INTO users (id, name, age) VALUES (123, 'John', 30)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert_eq!(insert.columns.len(), 3);
match &insert.values {
CqlInsertValues::Values(vals) => {
assert_eq!(vals.len(), 3);
}
_ => panic!("Expected Values variant"),
}
}
#[test]
fn test_parse_insert_with_ttl() {
let cql = "INSERT INTO users (id, name) VALUES (?, ?) USING TTL 3600";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert!(insert.using.is_some());
assert!(insert.using.as_ref().unwrap().ttl.is_some());
}
#[test]
fn test_parse_insert_with_timestamp() {
let cql = "INSERT INTO users (id, name) VALUES (?, ?) USING TIMESTAMP 12345";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert!(insert.using.is_some());
assert!(insert.using.as_ref().unwrap().timestamp.is_some());
}
#[test]
fn test_parse_insert_if_not_exists() {
let cql = "INSERT INTO users (id, name) VALUES (?, ?) IF NOT EXISTS";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert!(insert.if_not_exists);
}
#[test]
fn test_parse_simple_update() {
let cql = "UPDATE users SET name = ? WHERE id = ?";
let result = parse_update_statement(cql);
assert!(result.is_ok());
let update = result.unwrap();
assert_eq!(update.table.name.name, "users");
assert_eq!(update.assignments.len(), 1);
assert_eq!(update.assignments[0].column.name, "name");
}
#[test]
fn test_parse_update_with_multiple_assignments() {
let cql = "UPDATE users SET name = ?, age = ? WHERE id = ?";
let result = parse_update_statement(cql);
assert!(result.is_ok());
let update = result.unwrap();
assert_eq!(update.assignments.len(), 2);
}
#[test]
fn test_parse_update_with_ttl() {
let cql = "UPDATE users USING TTL 3600 SET name = ? WHERE id = ?";
let result = parse_update_statement(cql);
assert!(result.is_ok());
let update = result.unwrap();
assert!(update.using.is_some());
assert!(update.using.as_ref().unwrap().ttl.is_some());
}
#[test]
fn test_parse_simple_delete() {
let cql = "DELETE FROM users WHERE id = ?";
let result = parse_delete_statement(cql);
if result.is_err() {
eprintln!("Parse error: {:?}", result.as_ref().err());
}
assert!(result.is_ok());
let delete = result.unwrap();
assert_eq!(delete.table.name.name, "users");
assert!(delete.columns.is_empty());
}
#[test]
fn test_parse_delete_columns() {
let cql = "DELETE name, age FROM users WHERE id = ?";
let result = parse_delete_statement(cql);
assert!(result.is_ok());
let delete = result.unwrap();
assert_eq!(delete.columns.len(), 2);
assert_eq!(delete.columns[0].name, "name");
assert_eq!(delete.columns[1].name, "age");
}
#[test]
fn test_parse_delete_with_timestamp() {
let cql = "DELETE FROM users USING TIMESTAMP 12345 WHERE id = ?";
let result = parse_delete_statement(cql);
assert!(result.is_ok());
let delete = result.unwrap();
assert!(delete.using.is_some());
assert!(delete.using.as_ref().unwrap().timestamp.is_some());
}
#[test]
fn test_parse_qualified_table_name() {
let cql = "INSERT INTO keyspace.users (id) VALUES (?)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert!(insert.table.keyspace.is_some());
assert_eq!(insert.table.keyspace.as_ref().unwrap().name, "keyspace");
assert_eq!(insert.table.name.name, "users");
}
#[test]
fn test_parse_quoted_identifiers() {
let cql = r#"INSERT INTO "MyTable" ("MyColumn") VALUES (?)"#;
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert!(insert.table.name.quoted);
assert_eq!(insert.table.name.name, "MyTable");
}
#[test]
fn test_parse_string_literals() {
let cql = "INSERT INTO users (name) VALUES ('John O''Brien')";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
}
#[test]
fn test_parse_null_literal() {
let cql = "INSERT INTO users (name) VALUES (null)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
match &insert.values {
CqlInsertValues::Values(vals) => {
assert_eq!(vals.len(), 1);
match &vals[0] {
CqlExpression::Literal(CqlLiteral::Null) => {}
_ => panic!("Expected NULL literal"),
}
}
_ => panic!("Expected Values variant"),
}
}
#[test]
fn test_parse_collection_literals() {
let cql = "INSERT INTO users (tags) VALUES (['tag1', 'tag2'])";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
}
#[test]
fn test_parse_error_invalid_syntax() {
let cql = "INSERT INVALID SYNTAX";
let result = parse_insert_statement(cql);
assert!(result.is_err());
}
#[test]
fn test_parse_insert_with_both_ttl_and_timestamp() {
let cql = "INSERT INTO users (id, name) VALUES (?, ?) USING TTL 3600 AND TIMESTAMP 12345";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert!(insert.using.is_some());
let using = insert.using.as_ref().unwrap();
assert!(using.ttl.is_some());
assert!(using.timestamp.is_some());
}
#[test]
fn test_parse_update_with_compound_where() {
let cql = "UPDATE users SET name = ? WHERE id = ? AND age = ?";
let result = parse_update_statement(cql);
assert!(result.is_ok());
let update = result.unwrap();
assert!(matches!(update.where_clause, CqlExpression::Binary { .. }));
}
#[test]
fn test_parse_delete_with_if_condition() {
let cql = "DELETE FROM users WHERE id = ? IF name = ?";
let result = parse_delete_statement(cql);
assert!(result.is_ok());
let delete = result.unwrap();
assert!(delete.if_condition.is_some());
}
#[test]
fn test_parse_update_with_add_assign() {
let cql = "UPDATE counters SET count += 1 WHERE id = ?";
let result = parse_update_statement(cql);
assert!(result.is_ok());
let update = result.unwrap();
assert_eq!(update.assignments.len(), 1);
assert!(matches!(
update.assignments[0].operator,
CqlAssignmentOperator::AddAssign
));
}
#[test]
fn test_parse_update_with_sub_assign() {
let cql = "UPDATE counters SET count -= 1 WHERE id = ?";
let result = parse_update_statement(cql);
assert!(result.is_ok());
let update = result.unwrap();
assert_eq!(update.assignments.len(), 1);
assert!(matches!(
update.assignments[0].operator,
CqlAssignmentOperator::SubAssign
));
}
#[test]
fn test_parse_named_parameters() {
let cql = "INSERT INTO users (id, name) VALUES (:id, :name)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
match &insert.values {
CqlInsertValues::Values(vals) => {
assert_eq!(vals.len(), 2);
assert!(matches!(vals[0], CqlExpression::NamedParameter(_)));
assert!(matches!(vals[1], CqlExpression::NamedParameter(_)));
}
_ => panic!("Expected Values variant"),
}
}
#[test]
fn test_parse_boolean_literals() {
let cql = "INSERT INTO users (id, active) VALUES (?, true)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
}
#[test]
fn test_parse_uuid_literal() {
let cql = "INSERT INTO users (id) VALUES (550e8400-e29b-41d4-a716-446655440000)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
match &insert.values {
CqlInsertValues::Values(vals) => {
assert_eq!(vals.len(), 1);
match &vals[0] {
CqlExpression::Literal(CqlLiteral::Uuid(_)) => {}
_ => panic!("Expected UUID literal"),
}
}
_ => panic!("Expected Values variant"),
}
}
#[test]
fn test_parse_blob_literal() {
let cql = "INSERT INTO users (data) VALUES (0xdeadbeef)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
match &insert.values {
CqlInsertValues::Values(vals) => {
assert_eq!(vals.len(), 1);
match &vals[0] {
CqlExpression::Literal(CqlLiteral::Blob(hex)) => {
assert_eq!(hex, "deadbeef");
}
_ => panic!("Expected Blob literal"),
}
}
_ => panic!("Expected Values variant"),
}
}
#[test]
fn test_parse_set_literal() {
let cql = "INSERT INTO users (tags) VALUES ({1, 2, 3})";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
}
#[test]
fn test_parse_map_literal() {
let cql = "INSERT INTO users (settings) VALUES ({'key': 'value'})";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
}
#[test]
#[allow(clippy::approx_constant)]
fn test_parse_float_literal() {
let cql = "INSERT INTO metrics (value) VALUES (3.14)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
match &insert.values {
CqlInsertValues::Values(vals) => {
assert_eq!(vals.len(), 1);
match &vals[0] {
CqlExpression::Literal(CqlLiteral::Float(f)) => {
assert!((*f - 3.14).abs() < 0.001, "Expected float close to 3.14");
}
_ => panic!("Expected Float literal"),
}
}
_ => panic!("Expected Values variant"),
}
}
#[test]
fn test_parse_negative_integer() {
let cql = "INSERT INTO metrics (value) VALUES (-42)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
match &insert.values {
CqlInsertValues::Values(vals) => {
assert_eq!(vals.len(), 1);
match &vals[0] {
CqlExpression::Literal(CqlLiteral::Integer(i)) => {
assert_eq!(*i, -42);
}
_ => panic!("Expected Integer literal"),
}
}
_ => panic!("Expected Values variant"),
}
}
#[test]
fn test_parse_escaped_string() {
let cql = r#"INSERT INTO users (name) VALUES ('O''Brien')"#;
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
match &insert.values {
CqlInsertValues::Values(vals) => {
assert_eq!(vals.len(), 1);
match &vals[0] {
CqlExpression::Literal(CqlLiteral::String(s)) => {
assert_eq!(s, "O'Brien");
}
_ => panic!("Expected String literal"),
}
}
_ => panic!("Expected Values variant"),
}
}
#[test]
fn test_parse_comparison_operators() {
let operators = vec![
("id = ?", CqlBinaryOperator::Eq),
("id != ?", CqlBinaryOperator::Ne),
("id < ?", CqlBinaryOperator::Lt),
("id <= ?", CqlBinaryOperator::Le),
("id > ?", CqlBinaryOperator::Gt),
("id >= ?", CqlBinaryOperator::Ge),
];
for (where_expr, expected_op) in operators {
let cql = format!("UPDATE users SET name = ? WHERE {}", where_expr);
let result = parse_update_statement(&cql);
assert!(result.is_ok(), "Failed to parse: {}", cql);
let update = result.unwrap();
match &update.where_clause {
CqlExpression::Binary { operator, .. } => {
assert_eq!(operator, &expected_op);
}
_ => panic!("Expected Binary expression"),
}
}
}
#[test]
fn test_input_length_limit() {
let large_input = format!(
"INSERT INTO users (id, name) VALUES (?, '{}')",
"a".repeat(17 * 1024 * 1024)
);
let result = parse_insert_statement(&large_input);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("input_length"));
}
#[test]
fn test_nesting_depth_limit() {
let mut nested = "1".to_string();
for _ in 0..40 {
nested = format!("[{}]", nested);
}
let cql = format!("INSERT INTO users (data) VALUES ({})", nested);
let result = parse_insert_statement(&cql);
assert!(result.is_err());
}
#[test]
fn test_collection_size_limit() {
let items: Vec<String> = (0..=MAX_COLLECTION_SIZE).map(|i| i.to_string()).collect();
let list = format!("[{}]", items.join(", "));
let cql = format!("INSERT INTO users (data) VALUES ({})", list);
let result = parse_insert_statement(&cql);
assert!(result.is_err());
}
#[test]
fn test_valid_nested_collections() {
let cql = "INSERT INTO users (data) VALUES ([[1, 2], [3, 4]])";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
}
#[test]
fn test_uuid_strict_format_valid() {
let cql = "INSERT INTO users (id) VALUES (550e8400-e29b-41d4-a716-446655440000)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
}
#[test]
fn test_uuid_invalid_segment_length() {
let cql = "INSERT INTO users (id) VALUES (550e8400-e29b-41d4-a716-4466554400)"; let result = parse_insert_statement(cql);
assert!(result.is_err());
}
#[test]
fn test_uuid_missing_dashes() {
let cql = "INSERT INTO users (id) VALUES (550e8400e29b41d4a716446655440000)";
let result = parse_insert_statement(cql);
assert!(result.is_err());
}
#[test]
fn test_blob_even_hex_length_valid() {
let cql = "INSERT INTO users (data) VALUES (0xdeadbeef)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
}
#[test]
fn test_blob_odd_hex_length_invalid() {
let cql = "INSERT INTO users (data) VALUES (0xabc)";
let result = parse_insert_statement(cql);
assert!(result.is_err());
}
#[test]
fn test_quoted_identifier_with_escaped_quotes() {
let cql = r#"INSERT INTO "My""Table" ("My""Column") VALUES (?)"#;
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert_eq!(insert.table.name.name, r#"My"Table"#);
assert_eq!(insert.columns[0].name, r#"My"Column"#);
}
#[test]
fn test_identifier_max_length() {
let valid_name = "a".repeat(48);
let cql = format!(r#"INSERT INTO "{}" (id) VALUES (?)"#, valid_name);
let result = parse_insert_statement(&cql);
assert!(result.is_ok());
}
#[test]
fn test_identifier_exceeds_max_length() {
let invalid_name = "a".repeat(49);
let cql = format!(r#"INSERT INTO "{}" (id) VALUES (?)"#, invalid_name);
let result = parse_insert_statement(&cql);
assert!(result.is_err());
}
#[test]
fn test_identifier_with_control_characters() {
let cql = "INSERT INTO \"bad\ntable\" (id) VALUES (?)";
let result = parse_insert_statement(cql);
assert!(result.is_err());
}
#[test]
fn test_identifier_with_null_byte() {
let cql = "INSERT INTO \"bad\0table\" (id) VALUES (?)";
let result = parse_insert_statement(cql);
assert!(result.is_err());
}
#[test]
fn test_valid_unquoted_identifier() {
let cql = "INSERT INTO my_table_123 (id) VALUES (?)";
let result = parse_insert_statement(cql);
assert!(result.is_ok());
}
#[test]
fn test_sanitize_for_filesystem() {
assert_eq!(sanitize_for_filesystem("normal"), "normal");
assert_eq!(sanitize_for_filesystem("with/slash"), "with_slash");
assert_eq!(sanitize_for_filesystem("with\\backslash"), "with_backslash");
assert_eq!(sanitize_for_filesystem("with:colon"), "with_colon");
assert_eq!(sanitize_for_filesystem("with*asterisk"), "with_asterisk");
assert_eq!(sanitize_for_filesystem("with?question"), "with_question");
assert_eq!(sanitize_for_filesystem("with\"quote"), "with_quote");
assert_eq!(sanitize_for_filesystem("with<less"), "with_less");
assert_eq!(sanitize_for_filesystem("with>greater"), "with_greater");
assert_eq!(sanitize_for_filesystem("with|pipe"), "with_pipe");
assert_eq!(sanitize_for_filesystem("with\0null"), "with_null");
let long_name = "a".repeat(100);
let sanitized = sanitize_for_filesystem(&long_name);
assert_eq!(sanitized.len(), MAX_IDENTIFIER_LENGTH);
}
#[test]
fn test_quoted_identifier_empty() {
let cql = r#"INSERT INTO "" (id) VALUES (?)"#;
let result = parse_insert_statement(cql);
assert!(result.is_err());
}
#[test]
fn test_multiple_escaped_quotes() {
let cql = r#"INSERT INTO "Tab""""le" (id) VALUES (?)"#;
let result = parse_insert_statement(cql);
assert!(result.is_ok());
let insert = result.unwrap();
assert_eq!(insert.table.name.name, r#"Tab""le"#);
}
#[test]
fn test_collection_size_within_limit() {
let items: Vec<String> = (0..1000).map(|i| i.to_string()).collect();
let list = format!("[{}]", items.join(", "));
let cql = format!("INSERT INTO users (data) VALUES ({})", list);
let result = parse_insert_statement(&cql);
assert!(result.is_ok());
}
#[test]
fn test_map_size_limit() {
let pairs: Vec<String> = (0..=MAX_COLLECTION_SIZE)
.map(|i| format!("{}: {}", i, i))
.collect();
let map = format!("{{{}}}", pairs.join(", "));
let cql = format!("INSERT INTO users (data) VALUES ({})", map);
let result = parse_insert_statement(&cql);
assert!(result.is_err());
}
#[test]
fn test_nested_map_depth_limit() {
let mut nested = "{'k': 1}".to_string();
for i in 0..40 {
nested = format!("{{'key{}': {}}}", i, nested);
}
let cql = format!("INSERT INTO users (data) VALUES ({})", nested);
let result = parse_insert_statement(&cql);
assert!(result.is_err());
}
#[test]
fn test_string_literal_with_multibyte_utf8() {
let cql = "INSERT INTO ks.t (name) VALUES ('héllo wörld')";
let result = parse_insert_statement(cql);
assert!(
result.is_ok(),
"Failed to parse string with accented chars: {:?}",
result.err()
);
}
#[test]
fn test_string_literal_with_emoji() {
let cql = "INSERT INTO ks.t (name) VALUES ('hello 🌍 world')";
let result = parse_insert_statement(cql);
assert!(
result.is_ok(),
"Failed to parse string with emoji: {:?}",
result.err()
);
}
#[test]
fn test_string_literal_with_cjk() {
let cql = "INSERT INTO ks.t (name) VALUES ('你好世界')";
let result = parse_insert_statement(cql);
assert!(
result.is_ok(),
"Failed to parse string with CJK chars: {:?}",
result.err()
);
}
#[test]
fn test_quoted_identifier_with_multibyte_utf8() {
let cql = "INSERT INTO ks.t (\"nàme\") VALUES ('test')";
let result = parse_insert_statement(cql);
assert!(
result.is_ok(),
"Failed to parse quoted identifier with accented chars: {:?}",
result.err()
);
}
#[test]
fn test_quoted_identifier_with_emoji() {
let cql = "INSERT INTO ks.t (\"col_🎉\") VALUES ('test')";
let result = parse_insert_statement(cql);
assert!(
result.is_ok(),
"Failed to parse quoted identifier with emoji: {:?}",
result.err()
);
}
#[test]
fn test_insert_json_basic() {
let cql = r#"INSERT INTO ks.t JSON '{"id": 1, "name": "test"}'"#;
let result = parse_insert_statement(cql).unwrap();
assert_eq!(result.table.keyspace.unwrap().name, "ks");
assert_eq!(result.table.name.name, "t");
assert!(result.columns.is_empty());
match &result.values {
CqlInsertValues::Json(s) => {
assert_eq!(s, r#"{"id": 1, "name": "test"}"#);
}
CqlInsertValues::Values(_) => panic!("Expected Json variant"),
}
}
#[test]
fn test_insert_json_if_not_exists() {
let cql = r#"INSERT INTO ks.t JSON '{"id": 1}' IF NOT EXISTS"#;
let result = parse_insert_statement(cql).unwrap();
assert!(result.if_not_exists);
assert!(matches!(&result.values, CqlInsertValues::Json(_)));
}
#[test]
fn test_insert_json_with_using_timestamp() {
let cql = r#"INSERT INTO ks.t JSON '{"id": 1}' USING TIMESTAMP 12345"#;
let result = parse_insert_statement(cql).unwrap();
assert!(matches!(&result.values, CqlInsertValues::Json(_)));
assert!(result.using.is_some());
}
#[test]
fn test_insert_values_still_works() {
let cql = "INSERT INTO ks.t (id, name) VALUES (1, 'test')";
let result = parse_insert_statement(cql).unwrap();
assert!(matches!(&result.values, CqlInsertValues::Values(_)));
assert_eq!(result.columns.len(), 2);
}
}