use crate::partiql::ast::*;
use crate::{Error, Result};
use sqlparser::ast::{self as sql_ast, Statement};
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser as SqlParser;
pub struct PartiQLParser;
impl PartiQLParser {
pub fn parse(sql: &str) -> Result<PartiQLStatement> {
if sql.is_empty() {
return Err(Error::InvalidQuery("Statement cannot be empty".into()));
}
if sql.len() > 8192 {
return Err(Error::InvalidQuery(format!(
"Statement too long: {} chars (max 8192)",
sql.len()
)));
}
if sql.trim().to_uppercase().starts_with("INSERT") && sql.contains('{') {
return Self::parse_insert_with_json_map(sql);
}
if sql.trim().to_uppercase().starts_with("UPDATE") && sql.to_uppercase().contains(" REMOVE ") {
return Self::parse_update_with_remove(sql);
}
let normalized_sql = sql.replace(" VALUE ", " VALUES ");
let dialect = GenericDialect {};
let statements = SqlParser::parse_sql(&dialect, &normalized_sql).map_err(|e| {
Error::InvalidQuery(format!("Failed to parse SQL: {}", e))
})?;
if statements.is_empty() {
return Err(Error::InvalidQuery("No statement found".into()));
}
if statements.len() > 1 {
return Err(Error::InvalidQuery("Multiple statements not supported".into()));
}
Self::convert_statement(&statements[0])
}
fn parse_insert_with_json_map(sql: &str) -> Result<PartiQLStatement> {
let sql_upper = sql.to_uppercase();
let into_idx = sql_upper.find("INTO ").ok_or_else(|| {
Error::InvalidQuery("INSERT requires INTO clause".into())
})?;
let value_idx = sql_upper.find(" VALUE ").or_else(|| sql_upper.find(" VALUES ")).ok_or_else(|| {
Error::InvalidQuery("INSERT requires VALUE clause".into())
})?;
let table_part = sql[into_idx + 5..value_idx].trim();
let table_name = table_part.split_whitespace().next().ok_or_else(|| {
Error::InvalidQuery("Could not extract table name".into())
})?.to_string();
let brace_start = sql.find('{').ok_or_else(|| {
Error::InvalidQuery("Expected JSON map starting with {".into())
})?;
let brace_end = sql.rfind('}').ok_or_else(|| {
Error::InvalidQuery("Expected JSON map ending with }".into())
})?;
if brace_end <= brace_start {
return Err(Error::InvalidQuery("Invalid JSON map syntax".into()));
}
let json_str = &sql[brace_start..=brace_end];
let value_map = Self::parse_json_string(json_str)?;
Ok(PartiQLStatement::Insert(InsertStatement {
table_name,
value: value_map,
}))
}
fn convert_statement(stmt: &Statement) -> Result<PartiQLStatement> {
match stmt {
Statement::Query(query) => {
let select_stmt = Self::convert_select(query)?;
Ok(PartiQLStatement::Select(select_stmt))
}
Statement::Insert(insert) => {
let insert_stmt = Self::convert_insert(insert)?;
Ok(PartiQLStatement::Insert(insert_stmt))
}
Statement::Update {
table,
assignments,
selection,
..
} => {
let update_stmt = Self::convert_update(table, assignments, selection, &[])?;
Ok(PartiQLStatement::Update(update_stmt))
}
Statement::Delete(delete) => {
let delete_stmt = Self::convert_delete(delete)?;
Ok(PartiQLStatement::Delete(delete_stmt))
}
_ => Err(Error::InvalidQuery(format!(
"Unsupported statement type: {:?}",
stmt
))),
}
}
fn convert_select(query: &sql_ast::Query) -> Result<SelectStatement> {
if query.with.is_some() {
return Err(Error::InvalidQuery("WITH clause not supported".into()));
}
if query.fetch.is_some() {
return Err(Error::InvalidQuery("FETCH clause not supported".into()));
}
let set_expr = match &*query.body {
sql_ast::SetExpr::Select(select) => select,
_ => return Err(Error::InvalidQuery("Unsupported query type (no UNION/INTERSECT/EXCEPT)".into())),
};
if !set_expr.cluster_by.is_empty() {
return Err(Error::InvalidQuery("CLUSTER BY not supported".into()));
}
if !set_expr.distribute_by.is_empty() {
return Err(Error::InvalidQuery("DISTRIBUTE BY not supported".into()));
}
if set_expr.group_by != sql_ast::GroupByExpr::Expressions(vec![], vec![]) {
return Err(Error::InvalidQuery("GROUP BY not supported".into()));
}
if set_expr.having.is_some() {
return Err(Error::InvalidQuery("HAVING clause not supported".into()));
}
if !set_expr.named_window.is_empty() {
return Err(Error::InvalidQuery("Window functions not supported".into()));
}
if !set_expr.qualify.is_none() {
return Err(Error::InvalidQuery("QUALIFY clause not supported".into()));
}
if let Some(_top) = &set_expr.top {
return Err(Error::InvalidQuery("TOP clause not supported".into()));
}
let (table_name, index_name) = if set_expr.from.is_empty() {
return Err(Error::InvalidQuery("FROM clause required".into()));
} else if set_expr.from.len() > 1 {
return Err(Error::InvalidQuery("Multiple tables not supported (no JOINs)".into()));
} else {
Self::extract_table_reference(&set_expr.from[0])?
};
let select_list = Self::convert_select_list(&set_expr.projection)?;
let where_clause = match &set_expr.selection {
Some(expr) => Some(Self::convert_where_clause(expr)?),
None => None,
};
let order_by = match &query.order_by {
Some(order_by_clause) => Some(Self::convert_order_by(&order_by_clause.exprs)?),
None => None,
};
let limit = match &query.limit {
Some(expr) => Some(Self::extract_limit_value(expr)?),
None => None,
};
let offset = match &query.offset {
Some(offset_expr) => Some(Self::extract_offset_value(&offset_expr.value)?),
None => None,
};
Ok(SelectStatement {
table_name,
index_name,
select_list,
where_clause,
order_by,
limit,
offset,
})
}
fn extract_table_reference(from: &sql_ast::TableWithJoins) -> Result<(String, Option<String>)> {
if !from.joins.is_empty() {
return Err(Error::InvalidQuery("JOIN not supported".into()));
}
match &from.relation {
sql_ast::TableFactor::Table { name, .. } => {
let table_parts: Vec<&sql_ast::Ident> = name.0.iter().collect();
if table_parts.is_empty() {
return Err(Error::InvalidQuery("Empty table name".into()));
}
if table_parts.len() == 1 {
Ok((table_parts[0].value.clone(), None))
} else if table_parts.len() == 2 {
Ok((table_parts[0].value.clone(), Some(table_parts[1].value.clone())))
} else {
Err(Error::InvalidQuery(format!(
"Invalid table reference: expected 'table' or 'table.index', got {:?}",
name
)))
}
}
_ => Err(Error::InvalidQuery("Unsupported FROM clause (subqueries not allowed)".into())),
}
}
fn convert_select_list(projection: &[sql_ast::SelectItem]) -> Result<SelectList> {
if projection.is_empty() {
return Err(Error::InvalidQuery("Empty SELECT list".into()));
}
if projection.len() == 1 {
if let sql_ast::SelectItem::Wildcard(_) = &projection[0] {
return Ok(SelectList::All);
}
}
let mut attributes = Vec::new();
for item in projection {
match item {
sql_ast::SelectItem::UnnamedExpr(expr) => {
let attr_name = Self::extract_attribute_name(expr)?;
attributes.push(attr_name);
}
sql_ast::SelectItem::ExprWithAlias { expr, alias: _ } => {
let attr_name = Self::extract_attribute_name(expr)?;
attributes.push(attr_name);
}
sql_ast::SelectItem::Wildcard(_) => {
return Err(Error::InvalidQuery("Cannot mix * with other columns".into()));
}
_ => {
return Err(Error::InvalidQuery("Unsupported SELECT item".into()));
}
}
}
Ok(SelectList::Attributes(attributes))
}
fn extract_attribute_name(expr: &sql_ast::Expr) -> Result<String> {
match expr {
sql_ast::Expr::Identifier(ident) => Ok(ident.value.clone()),
sql_ast::Expr::CompoundIdentifier(parts) => {
if parts.len() == 1 {
Ok(parts[0].value.clone())
} else {
Err(Error::InvalidQuery(format!(
"Compound identifiers not supported: {:?}",
parts
)))
}
}
_ => Err(Error::InvalidQuery(format!(
"Unsupported expression in SELECT list: {:?}",
expr
))),
}
}
fn convert_where_clause(expr: &sql_ast::Expr) -> Result<WhereClause> {
let mut conditions = Vec::new();
Self::extract_conditions(expr, &mut conditions)?;
Ok(WhereClause { conditions })
}
fn extract_conditions(expr: &sql_ast::Expr, conditions: &mut Vec<Condition>) -> Result<()> {
match expr {
sql_ast::Expr::BinaryOp { left, op, right } => {
use sqlparser::ast::BinaryOperator;
match op {
BinaryOperator::And => {
Self::extract_conditions(left, conditions)?;
Self::extract_conditions(right, conditions)?;
}
BinaryOperator::Or => {
return Err(Error::InvalidQuery("OR not supported in WHERE clause (use AND only)".into()));
}
BinaryOperator::Eq
| BinaryOperator::NotEq
| BinaryOperator::Lt
| BinaryOperator::LtEq
| BinaryOperator::Gt
| BinaryOperator::GtEq => {
let condition = Self::convert_comparison(left, op, right)?;
conditions.push(condition);
}
_ => {
return Err(Error::InvalidQuery(format!(
"Unsupported operator in WHERE clause: {:?}",
op
)));
}
}
}
sql_ast::Expr::InList { expr, list, negated } => {
if *negated {
return Err(Error::InvalidQuery("NOT IN not supported".into()));
}
let condition = Self::convert_in_condition(expr, list)?;
conditions.push(condition);
}
sql_ast::Expr::Between { expr, negated, low, high } => {
if *negated {
return Err(Error::InvalidQuery("NOT BETWEEN not supported".into()));
}
let condition = Self::convert_between_condition(expr, low, high)?;
conditions.push(condition);
}
_ => {
return Err(Error::InvalidQuery(format!(
"Unsupported WHERE clause expression: {:?}",
expr
)));
}
}
Ok(())
}
fn convert_comparison(
left: &sql_ast::Expr,
op: &sql_ast::BinaryOperator,
right: &sql_ast::Expr,
) -> Result<Condition> {
use sqlparser::ast::BinaryOperator;
let attribute = Self::extract_attribute_name(left)?;
let compare_op = match op {
BinaryOperator::Eq => CompareOp::Equal,
BinaryOperator::NotEq => CompareOp::NotEqual,
BinaryOperator::Lt => CompareOp::LessThan,
BinaryOperator::LtEq => CompareOp::LessThanOrEqual,
BinaryOperator::Gt => CompareOp::GreaterThan,
BinaryOperator::GtEq => CompareOp::GreaterThanOrEqual,
_ => unreachable!(),
};
let value = Self::convert_value(right)?;
Ok(Condition {
attribute,
operator: compare_op,
value,
})
}
fn convert_in_condition(
expr: &sql_ast::Expr,
list: &[sql_ast::Expr],
) -> Result<Condition> {
let attribute = Self::extract_attribute_name(expr)?;
let values: Result<Vec<SqlValue>> = list.iter().map(Self::convert_value).collect();
Ok(Condition {
attribute,
operator: CompareOp::In,
value: SqlValue::List(values?),
})
}
fn convert_between_condition(
expr: &sql_ast::Expr,
low: &sql_ast::Expr,
high: &sql_ast::Expr,
) -> Result<Condition> {
let attribute = Self::extract_attribute_name(expr)?;
let low_val = Self::convert_value(low)?;
let high_val = Self::convert_value(high)?;
Ok(Condition {
attribute,
operator: CompareOp::Between,
value: SqlValue::List(vec![low_val, high_val]),
})
}
fn convert_value(expr: &sql_ast::Expr) -> Result<SqlValue> {
match expr {
sql_ast::Expr::Value(val) => Self::convert_sql_value(val),
_ => Err(Error::InvalidQuery(format!(
"Unsupported value expression: {:?}",
expr
))),
}
}
fn convert_sql_value(val: &sql_ast::Value) -> Result<SqlValue> {
match val {
sql_ast::Value::Number(n, _) => Ok(SqlValue::Number(n.clone())),
sql_ast::Value::SingleQuotedString(s) | sql_ast::Value::DoubleQuotedString(s) => {
Ok(SqlValue::String(s.clone()))
}
sql_ast::Value::Boolean(b) => Ok(SqlValue::Boolean(*b)),
sql_ast::Value::Null => Ok(SqlValue::Null),
_ => Err(Error::InvalidQuery(format!("Unsupported SQL value: {:?}", val))),
}
}
fn convert_order_by(order_by: &[sql_ast::OrderByExpr]) -> Result<OrderBy> {
if order_by.is_empty() {
return Err(Error::InvalidQuery("Empty ORDER BY clause".into()));
}
if order_by.len() > 1 {
return Err(Error::InvalidQuery("ORDER BY multiple columns not supported".into()));
}
let expr = &order_by[0];
let attribute = Self::extract_attribute_name(&expr.expr)?;
let ascending = expr.asc.unwrap_or(true);
Ok(OrderBy {
attribute,
ascending,
})
}
fn convert_insert(insert: &sql_ast::Insert) -> Result<InsertStatement> {
let table_name = match &insert.table_name {
sql_ast::ObjectName(parts) => {
if parts.is_empty() {
return Err(Error::InvalidQuery("Empty table name in INSERT".into()));
}
parts[0].value.clone()
}
};
let value_map = match &insert.source {
Some(source) => {
match &*source.body {
sql_ast::SetExpr::Values(values) => {
if values.rows.is_empty() {
return Err(Error::InvalidQuery("INSERT requires VALUE clause".into()));
}
if values.rows.len() > 1 {
return Err(Error::InvalidQuery(
"INSERT can only insert one item at a time".into(),
));
}
Self::parse_map_literal(&values.rows[0])?
}
_ => {
return Err(Error::InvalidQuery(
"INSERT only supports VALUE clause, not SELECT".into(),
));
}
}
}
None => return Err(Error::InvalidQuery("INSERT requires VALUE clause".into())),
};
Ok(InsertStatement {
table_name,
value: value_map,
})
}
fn parse_map_literal(row: &[sql_ast::Expr]) -> Result<SqlValue> {
if row.len() != 1 {
return Err(Error::InvalidQuery(format!(
"Expected single map literal in INSERT, got {} expressions",
row.len()
)));
}
match &row[0] {
sql_ast::Expr::Function(func) => {
Self::parse_function_as_map(func)
}
sql_ast::Expr::JsonAccess { .. } | sql_ast::Expr::CompositeAccess { .. } => {
Err(Error::InvalidQuery(
"Composite/JSON access expressions not yet supported for INSERT".into(),
))
}
sql_ast::Expr::Value(sql_ast::Value::SingleQuotedString(s)) => {
Self::parse_json_string(s)
}
_ => {
Err(Error::InvalidQuery(format!(
"Unsupported INSERT VALUE format. Expression type: {:?}",
row[0]
)))
}
}
}
fn parse_function_as_map(func: &sql_ast::Function) -> Result<SqlValue> {
let mut map = std::collections::HashMap::new();
let args_list = match &func.args {
sql_ast::FunctionArguments::List(args) => &args.args,
_ => return Err(Error::InvalidQuery("Expected argument list in function".into())),
};
for arg in args_list {
match arg {
sql_ast::FunctionArg::Unnamed(expr_wrapper) => {
let expr = match expr_wrapper {
sql_ast::FunctionArgExpr::Expr(e) => e,
_ => return Err(Error::InvalidQuery("Expected expression in argument".into())),
};
if let sql_ast::Expr::BinaryOp { left, op, right } = expr {
if matches!(op, sql_ast::BinaryOperator::Eq) {
let key = Self::extract_string_literal(&**left)?;
let value = Self::convert_value(&**right)?;
map.insert(key, value);
} else {
return Err(Error::InvalidQuery(
"Expected key = value pairs in map".into(),
));
}
} else {
return Err(Error::InvalidQuery(
"Expected key = value pairs in map".into(),
));
}
}
_ => {
return Err(Error::InvalidQuery(
"Unsupported function argument in map".into(),
));
}
}
}
Ok(SqlValue::Map(map))
}
fn extract_string_literal(expr: &sql_ast::Expr) -> Result<String> {
match expr {
sql_ast::Expr::Value(sql_ast::Value::SingleQuotedString(s))
| sql_ast::Expr::Value(sql_ast::Value::DoubleQuotedString(s)) => Ok(s.clone()),
sql_ast::Expr::Identifier(ident) => Ok(ident.value.clone()),
_ => Err(Error::InvalidQuery(format!(
"Expected string literal, got: {:?}",
expr
))),
}
}
fn extract_limit_value(expr: &sql_ast::Expr) -> Result<usize> {
match expr {
sql_ast::Expr::Value(sql_ast::Value::Number(n, _)) => {
n.parse::<usize>().map_err(|_| {
Error::InvalidQuery(format!("Invalid LIMIT value: {}", n))
})
}
_ => Err(Error::InvalidQuery(format!(
"LIMIT must be a positive integer, got: {:?}",
expr
))),
}
}
fn extract_offset_value(expr: &sql_ast::Expr) -> Result<usize> {
match expr {
sql_ast::Expr::Value(sql_ast::Value::Number(n, _)) => {
n.parse::<usize>().map_err(|_| {
Error::InvalidQuery(format!("Invalid OFFSET value: {}", n))
})
}
_ => Err(Error::InvalidQuery(format!(
"OFFSET must be a positive integer, got: {:?}",
expr
))),
}
}
fn parse_json_string(s: &str) -> Result<SqlValue> {
let json_normalized = s.replace('\'', "\"");
let json_value: serde_json::Value = serde_json::from_str(&json_normalized).map_err(|e| {
Error::InvalidQuery(format!("Failed to parse JSON: {}", e))
})?;
Self::json_to_sql_value(&json_value)
}
fn json_to_sql_value(value: &serde_json::Value) -> Result<SqlValue> {
match value {
serde_json::Value::Null => Ok(SqlValue::Null),
serde_json::Value::Bool(b) => Ok(SqlValue::Boolean(*b)),
serde_json::Value::Number(n) => Ok(SqlValue::Number(n.to_string())),
serde_json::Value::String(s) => Ok(SqlValue::String(s.clone())),
serde_json::Value::Array(arr) => {
let items: Result<Vec<SqlValue>> = arr.iter().map(Self::json_to_sql_value).collect();
Ok(SqlValue::List(items?))
}
serde_json::Value::Object(obj) => {
let mut map = std::collections::HashMap::new();
for (k, v) in obj {
map.insert(k.clone(), Self::json_to_sql_value(v)?);
}
Ok(SqlValue::Map(map))
}
}
}
fn convert_delete(delete: &sql_ast::Delete) -> Result<DeleteStatement> {
let from_tables = match &delete.from {
sql_ast::FromTable::WithFromKeyword(tables) => tables,
sql_ast::FromTable::WithoutKeyword(tables) => tables,
};
if from_tables.is_empty() {
return Err(Error::InvalidQuery("DELETE requires table name".into()));
}
let (table_name, index_name) = Self::extract_table_reference(&from_tables[0])?;
if index_name.is_some() {
return Err(Error::InvalidQuery("DELETE does not support index syntax".into()));
}
let where_clause = match &delete.selection {
Some(expr) => Self::convert_where_clause(expr)?,
None => return Err(Error::InvalidQuery("DELETE requires WHERE clause".into())),
};
Ok(DeleteStatement {
table_name,
where_clause,
})
}
fn parse_update_with_remove(sql: &str) -> Result<PartiQLStatement> {
let sql_upper = sql.to_uppercase();
let remove_idx = sql_upper.find(" REMOVE ").ok_or_else(|| {
Error::InvalidQuery("Expected REMOVE clause".into())
})?;
let where_idx = sql_upper.find(" WHERE ").ok_or_else(|| {
Error::InvalidQuery("UPDATE requires WHERE clause".into())
})?;
if where_idx <= remove_idx {
return Err(Error::InvalidQuery("REMOVE must come before WHERE".into()));
}
let remove_part = &sql[remove_idx + 8..where_idx].trim();
let remove_attributes: Vec<String> = remove_part
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if remove_attributes.is_empty() {
return Err(Error::InvalidQuery("REMOVE requires at least one attribute".into()));
}
let before_remove = &sql[..remove_idx];
let has_set = before_remove.to_uppercase().contains(" SET ");
let sql_without_remove = if has_set {
format!(
"{} {}",
&sql[..remove_idx].trim(),
&sql[where_idx..].trim()
)
} else {
format!(
"{} SET __dummy__ = 0 {}",
&sql[..remove_idx].trim(),
&sql[where_idx..].trim()
)
};
let dialect = GenericDialect {};
let statements = SqlParser::parse_sql(&dialect, &sql_without_remove).map_err(|e| {
Error::InvalidQuery(format!("Failed to parse UPDATE: {}", e))
})?;
if statements.is_empty() {
return Err(Error::InvalidQuery("No statement found".into()));
}
match &statements[0] {
Statement::Update {
table,
assignments,
selection,
..
} => {
let update_stmt = Self::convert_update(table, assignments, selection, &remove_attributes)?;
Ok(PartiQLStatement::Update(update_stmt))
}
_ => Err(Error::InvalidQuery("Expected UPDATE statement".into())),
}
}
fn convert_update(
table: &sql_ast::TableWithJoins,
assignments: &[sql_ast::Assignment],
selection: &Option<sql_ast::Expr>,
remove_attributes: &[String],
) -> Result<UpdateStatement> {
let (table_name, index_name) = Self::extract_table_reference(table)?;
if index_name.is_some() {
return Err(Error::InvalidQuery("UPDATE does not support index syntax".into()));
}
let where_clause = match selection {
Some(expr) => Self::convert_where_clause(expr)?,
None => return Err(Error::InvalidQuery("UPDATE requires WHERE clause".into())),
};
let set_assignments: Vec<SetAssignment> = assignments
.iter()
.map(Self::convert_assignment)
.collect::<Result<Vec<_>>>()?
.into_iter()
.filter(|a| a.attribute != "__dummy__")
.collect();
Ok(UpdateStatement {
table_name,
where_clause,
set_assignments,
remove_attributes: remove_attributes.to_vec(),
})
}
fn convert_assignment(assignment: &sql_ast::Assignment) -> Result<SetAssignment> {
let attribute = match &assignment.target {
sql_ast::AssignmentTarget::ColumnName(name) => {
match name {
sql_ast::ObjectName(parts) => {
if parts.is_empty() {
return Err(Error::InvalidQuery("Empty attribute name".into()));
}
parts[0].value.clone()
}
}
}
_ => return Err(Error::InvalidQuery("Unsupported assignment target".into())),
};
let value = Self::convert_set_value(&assignment.value)?;
Ok(SetAssignment { attribute, value })
}
fn convert_set_value(expr: &sql_ast::Expr) -> Result<SetValue> {
match expr {
sql_ast::Expr::Value(v) => {
let sql_value = Self::convert_sql_value(v)?;
Ok(SetValue::Literal(sql_value))
}
sql_ast::Expr::BinaryOp { left, op, right } => {
let attribute = match &**left {
sql_ast::Expr::Identifier(ident) => ident.value.clone(),
_ => return Err(Error::InvalidQuery(
"Arithmetic expression left side must be an attribute".into()
)),
};
let value = match &**right {
sql_ast::Expr::Value(v) => Self::convert_sql_value(v)?,
_ => return Err(Error::InvalidQuery(
"Arithmetic expression right side must be a literal".into()
)),
};
match op {
sql_ast::BinaryOperator::Plus => Ok(SetValue::Add { attribute, value }),
sql_ast::BinaryOperator::Minus => Ok(SetValue::Subtract { attribute, value }),
_ => Err(Error::InvalidQuery(format!(
"Unsupported arithmetic operator in SET: {:?}",
op
))),
}
}
_ => Err(Error::InvalidQuery(format!(
"Unsupported SET value expression: {:?}",
expr
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_select() {
let sql = "SELECT * FROM users WHERE pk = 'user#123'";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Select(select) => {
assert_eq!(select.table_name, "users");
assert_eq!(select.index_name, None);
assert_eq!(select.select_list, SelectList::All);
let where_clause = select.where_clause.unwrap();
assert_eq!(where_clause.conditions.len(), 1);
assert_eq!(where_clause.conditions[0].attribute, "pk");
assert_eq!(where_clause.conditions[0].operator, CompareOp::Equal);
}
_ => panic!("Expected SELECT statement"),
}
}
#[test]
fn test_parse_select_with_index() {
let sql = "SELECT * FROM users.email_index WHERE pk = 'org#acme'";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Select(select) => {
assert_eq!(select.table_name, "users");
assert_eq!(select.index_name, Some("email_index".to_string()));
}
_ => panic!("Expected SELECT statement"),
}
}
#[test]
fn test_parse_select_with_attributes() {
let sql = "SELECT name, age FROM users WHERE pk = 'user#123'";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Select(select) => {
match select.select_list {
SelectList::Attributes(attrs) => {
assert_eq!(attrs, vec!["name", "age"]);
}
_ => panic!("Expected attribute list"),
}
}
_ => panic!("Expected SELECT statement"),
}
}
#[test]
fn test_parse_select_with_order_by() {
let sql = "SELECT * FROM users WHERE pk = 'user#123' ORDER BY sk DESC";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Select(select) => {
let order_by = select.order_by.unwrap();
assert_eq!(order_by.attribute, "sk");
assert_eq!(order_by.ascending, false);
}
_ => panic!("Expected SELECT statement"),
}
}
#[test]
fn test_parse_select_with_in() {
let sql = "SELECT * FROM users WHERE pk IN ('user#1', 'user#2')";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Select(select) => {
let where_clause = select.where_clause.unwrap();
assert_eq!(where_clause.conditions[0].operator, CompareOp::In);
}
_ => panic!("Expected SELECT statement"),
}
}
#[test]
fn test_parse_select_with_between() {
let sql = "SELECT * FROM users WHERE pk = 'user#123' AND age BETWEEN 18 AND 65";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Select(select) => {
let where_clause = select.where_clause.unwrap();
assert_eq!(where_clause.conditions.len(), 2);
let between_cond = where_clause.conditions.iter()
.find(|c| c.operator == CompareOp::Between)
.unwrap();
assert_eq!(between_cond.attribute, "age");
}
_ => panic!("Expected SELECT statement"),
}
}
#[test]
fn test_reject_join() {
let sql = "SELECT * FROM users JOIN orders ON users.pk = orders.user_id";
let result = PartiQLParser::parse(sql);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("JOIN"));
}
#[test]
fn test_reject_or() {
let sql = "SELECT * FROM users WHERE pk = 'user#123' OR pk = 'user#456'";
let result = PartiQLParser::parse(sql);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("OR"));
}
#[test]
fn test_reject_group_by() {
let sql = "SELECT COUNT(*) FROM users GROUP BY status";
let result = PartiQLParser::parse(sql);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("GROUP BY"));
}
#[test]
fn test_reject_too_long() {
let sql = "SELECT * FROM users WHERE pk = '".to_string() + &"x".repeat(10000) + "'";
let result = PartiQLParser::parse(&sql);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too long"));
}
#[test]
fn test_parse_delete_with_pk() {
let sql = "DELETE FROM users WHERE pk = 'user#123'";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Delete(delete) => {
assert_eq!(delete.table_name, "users");
assert_eq!(delete.where_clause.conditions.len(), 1);
assert_eq!(delete.where_clause.conditions[0].attribute, "pk");
assert_eq!(delete.where_clause.conditions[0].operator, CompareOp::Equal);
}
_ => panic!("Expected DELETE statement"),
}
}
#[test]
fn test_parse_delete_with_pk_and_sk() {
let sql = "DELETE FROM users WHERE pk = 'user#123' AND sk = 'profile'";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Delete(delete) => {
assert_eq!(delete.table_name, "users");
assert_eq!(delete.where_clause.conditions.len(), 2);
let pk_cond = delete.where_clause.get_condition("pk").unwrap();
assert_eq!(pk_cond.operator, CompareOp::Equal);
let sk_cond = delete.where_clause.get_condition("sk").unwrap();
assert_eq!(sk_cond.operator, CompareOp::Equal);
}
_ => panic!("Expected DELETE statement"),
}
}
#[test]
fn test_reject_delete_without_where() {
let sql = "DELETE FROM users";
let result = PartiQLParser::parse(sql);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("WHERE"));
}
#[test]
fn test_parse_insert_simple() {
let sql = "INSERT INTO users VALUE {'pk': 'user#123', 'name': 'Alice', 'age': 30}";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Insert(insert) => {
assert_eq!(insert.table_name, "users");
match &insert.value {
SqlValue::Map(map) => {
assert_eq!(map.len(), 3);
assert_eq!(map.get("pk"), Some(&SqlValue::String("user#123".to_string())));
assert_eq!(map.get("name"), Some(&SqlValue::String("Alice".to_string())));
assert_eq!(map.get("age"), Some(&SqlValue::Number("30".to_string())));
}
_ => panic!("Expected Map value"),
}
}
_ => panic!("Expected INSERT statement"),
}
}
#[test]
fn test_parse_insert_with_sk() {
let sql = "INSERT INTO users VALUE {'pk': 'user#123', 'sk': 'profile', 'email': 'alice@example.com'}";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Insert(insert) => {
assert_eq!(insert.table_name, "users");
match &insert.value {
SqlValue::Map(map) => {
assert!(map.contains_key("pk"));
assert!(map.contains_key("sk"));
assert_eq!(map.get("sk"), Some(&SqlValue::String("profile".to_string())));
}
_ => panic!("Expected Map value"),
}
}
_ => panic!("Expected INSERT statement"),
}
}
#[test]
fn test_parse_insert_nested_values() {
let sql = r#"INSERT INTO users VALUE {'pk': 'user#123', 'profile': {'name': 'Alice', 'age': 30}, 'tags': ['admin', 'active']}"#;
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Insert(insert) => {
match &insert.value {
SqlValue::Map(map) => {
match map.get("profile") {
Some(SqlValue::Map(profile)) => {
assert_eq!(profile.get("name"), Some(&SqlValue::String("Alice".to_string())));
}
_ => panic!("Expected nested map for profile"),
}
match map.get("tags") {
Some(SqlValue::List(tags)) => {
assert_eq!(tags.len(), 2);
}
_ => panic!("Expected list for tags"),
}
}
_ => panic!("Expected Map value"),
}
}
_ => panic!("Expected INSERT statement"),
}
}
#[test]
fn test_parse_insert_various_types() {
let sql = "INSERT INTO items VALUE {'pk': 'item#1', 'price': 29.99, 'active': true, 'description': null}";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Insert(insert) => {
match &insert.value {
SqlValue::Map(map) => {
match map.get("price") {
Some(SqlValue::Number(n)) => assert_eq!(n, "29.99"),
_ => panic!("Expected number for price"),
}
assert_eq!(map.get("active"), Some(&SqlValue::Boolean(true)));
assert_eq!(map.get("description"), Some(&SqlValue::Null));
}
_ => panic!("Expected Map value"),
}
}
_ => panic!("Expected INSERT statement"),
}
}
#[test]
fn test_reject_insert_without_map() {
let sql = "INSERT INTO users VALUE 'not a map'";
let result = PartiQLParser::parse(sql);
assert!(result.is_err());
}
#[test]
fn test_parse_update_simple() {
let sql = "UPDATE users SET name = 'Alice', age = 30 WHERE pk = 'user#123'";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Update(update) => {
assert_eq!(update.table_name, "users");
assert_eq!(update.set_assignments.len(), 2);
assert_eq!(update.remove_attributes.len(), 0);
assert_eq!(update.set_assignments[0].attribute, "name");
match &update.set_assignments[0].value {
SetValue::Literal(SqlValue::String(s)) => assert_eq!(s, "Alice"),
_ => panic!("Expected string literal"),
}
assert!(update.where_clause.has_condition("pk"));
}
_ => panic!("Expected UPDATE statement"),
}
}
#[test]
fn test_parse_update_with_arithmetic() {
let sql = "UPDATE users SET age = age + 1, count = count - 5 WHERE pk = 'user#123'";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Update(update) => {
assert_eq!(update.set_assignments.len(), 2);
match &update.set_assignments[0].value {
SetValue::Add { attribute, value } => {
assert_eq!(attribute, "age");
match value {
SqlValue::Number(n) => assert_eq!(n, "1"),
_ => panic!("Expected number"),
}
}
_ => panic!("Expected Add operation"),
}
match &update.set_assignments[1].value {
SetValue::Subtract { attribute, value } => {
assert_eq!(attribute, "count");
match value {
SqlValue::Number(n) => assert_eq!(n, "5"),
_ => panic!("Expected number"),
}
}
_ => panic!("Expected Subtract operation"),
}
}
_ => panic!("Expected UPDATE statement"),
}
}
#[test]
fn test_parse_update_with_remove() {
let sql = "UPDATE users SET name = 'Alice' REMOVE tags, metadata WHERE pk = 'user#123'";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Update(update) => {
assert_eq!(update.table_name, "users");
assert_eq!(update.set_assignments.len(), 1);
assert_eq!(update.remove_attributes.len(), 2);
assert_eq!(update.remove_attributes[0], "tags");
assert_eq!(update.remove_attributes[1], "metadata");
}
_ => panic!("Expected UPDATE statement"),
}
}
#[test]
fn test_parse_update_remove_only() {
let sql = "UPDATE users REMOVE tags, metadata WHERE pk = 'user#123' AND sk = 'profile'";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Update(update) => {
assert_eq!(update.set_assignments.len(), 0);
assert_eq!(update.remove_attributes.len(), 2);
assert_eq!(update.where_clause.conditions.len(), 2);
}
_ => panic!("Expected UPDATE statement"),
}
}
#[test]
fn test_reject_update_without_where() {
let sql = "UPDATE users SET name = 'Alice'";
let result = PartiQLParser::parse(sql);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("WHERE"));
}
#[test]
fn test_parse_select_with_limit() {
let sql = "SELECT * FROM users WHERE pk = 'user#123' LIMIT 10";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Select(select) => {
assert_eq!(select.table_name, "users");
assert_eq!(select.limit, Some(10));
assert_eq!(select.offset, None);
}
_ => panic!("Expected SELECT statement"),
}
}
#[test]
fn test_parse_select_with_offset() {
let sql = "SELECT * FROM users WHERE pk = 'user#123' OFFSET 5";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Select(select) => {
assert_eq!(select.table_name, "users");
assert_eq!(select.limit, None);
assert_eq!(select.offset, Some(5));
}
_ => panic!("Expected SELECT statement"),
}
}
#[test]
fn test_parse_select_with_limit_and_offset() {
let sql = "SELECT * FROM users WHERE pk = 'user#123' LIMIT 20 OFFSET 10";
let stmt = PartiQLParser::parse(sql).unwrap();
match stmt {
PartiQLStatement::Select(select) => {
assert_eq!(select.table_name, "users");
assert_eq!(select.limit, Some(20));
assert_eq!(select.offset, Some(10));
}
_ => panic!("Expected SELECT statement"),
}
}
}