use crate::types::AttributeValue;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ParseError {
#[error("{0}")]
Syntax(String),
#[error("{0}")]
Validation(String),
}
impl ParseError {
pub fn into_message(self) -> String {
match self {
ParseError::Syntax(m) => {
format!("Statement wasn't well formed, can't be processed: {m}")
}
ParseError::Validation(m) => m,
}
}
}
impl From<String> for ParseError {
fn from(message: String) -> Self {
ParseError::Syntax(message)
}
}
impl From<&str> for ParseError {
fn from(message: &str) -> Self {
ParseError::Syntax(message.to_string())
}
}
#[derive(Debug, Clone)]
pub enum Statement {
#[non_exhaustive]
Select {
table_name: String,
index_name: Option<String>,
projections: Vec<String>, where_clause: Option<WhereClause>,
},
#[non_exhaustive]
Insert {
table_name: String,
item: HashMap<String, PartiqlValue>,
if_not_exists: bool,
},
#[non_exhaustive]
Update {
table_name: String,
index_name: Option<String>,
set_clauses: Vec<SetClause>,
remove_paths: Vec<String>,
where_clause: Option<WhereClause>,
returning: Option<ReturningVariant>,
},
#[non_exhaustive]
Delete {
table_name: String,
index_name: Option<String>,
where_clause: Option<WhereClause>,
returning: Option<ReturningVariant>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReturningVariant {
AllOld,
ModifiedOld,
AllNew,
ModifiedNew,
}
impl ReturningVariant {
pub fn as_sql(self) -> &'static str {
match self {
ReturningVariant::AllOld => "ALL OLD",
ReturningVariant::ModifiedOld => "MODIFIED OLD",
ReturningVariant::AllNew => "ALL NEW",
ReturningVariant::ModifiedNew => "MODIFIED NEW",
}
}
}
pub fn table_name(stmt: &Statement) -> Option<&str> {
match stmt {
Statement::Select { table_name, .. }
| Statement::Insert { table_name, .. }
| Statement::Update { table_name, .. }
| Statement::Delete { table_name, .. } => Some(table_name),
}
}
pub fn index_name(stmt: &Statement) -> Option<&str> {
match stmt {
Statement::Select { index_name, .. }
| Statement::Update { index_name, .. }
| Statement::Delete { index_name, .. } => index_name.as_deref(),
Statement::Insert { .. } => None,
}
}
pub fn returning_variant(stmt: &Statement) -> Option<ReturningVariant> {
match stmt {
Statement::Update { returning, .. } | Statement::Delete { returning, .. } => *returning,
_ => None,
}
}
pub(crate) fn count_projection_rejection(statement: &str) -> Option<String> {
fn advance(c: char, line: &mut usize, col: &mut usize) {
if c == '\n' {
*line += 1;
*col = 1;
} else {
*col += 1;
}
}
let chars: Vec<char> = statement.chars().collect();
let len = chars.len();
let mut i = 0;
let mut line = 1usize;
let mut col = 1usize;
let mut first_word_seen = false;
while i < len {
let c = chars[i];
if c == '\'' || c == '"' {
let quote = c;
advance(c, &mut line, &mut col);
i += 1;
while i < len {
let ch = chars[i];
advance(ch, &mut line, &mut col);
i += 1;
if ch == quote {
if i < len && chars[i] == quote {
advance(chars[i], &mut line, &mut col);
i += 1; } else {
break;
}
}
}
continue;
}
if c.is_ascii_alphabetic() || c == '_' {
let word_line = line;
let word_col = col;
let start = i;
while i < len && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
advance(chars[i], &mut line, &mut col);
i += 1;
}
let word: String = chars[start..i].iter().collect();
if !first_word_seen {
first_word_seen = true;
if !word.eq_ignore_ascii_case("SELECT") {
return None;
}
continue;
}
if word.eq_ignore_ascii_case("FROM") {
return None;
}
if word.eq_ignore_ascii_case("COUNT") {
let mut j = i;
while j < len && chars[j].is_ascii_whitespace() {
j += 1;
}
if j < len && chars[j] == '(' {
return Some(format!(
"Unexpected path component at {word_line}:{word_col}:5"
));
}
}
continue;
}
advance(c, &mut line, &mut col);
i += 1;
}
None
}
#[derive(Debug, Clone)]
pub struct SetClause {
pub path: String,
pub value: SetValue,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SetValue {
Simple(PartiqlValue),
Add(String, PartiqlValue),
Sub(String, PartiqlValue),
ListAppend(PartiqlValue, PartiqlValue),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct WhereClause {
pub groups: Vec<Vec<WhereCondition>>,
pub wrote_or: bool,
}
impl WhereClause {
pub fn from_groups_written(groups: Vec<Vec<WhereCondition>>, wrote_or: bool) -> Self {
Self { groups, wrote_or }
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum WhereCondition {
Comparison(Condition),
NotComparison(Condition),
Exists(String),
NotExists(String),
BeginsWith(String, PartiqlValue),
NotBeginsWith(String, PartiqlValue),
Between(String, PartiqlValue, PartiqlValue),
In(String, Vec<PartiqlValue>),
Contains(String, PartiqlValue),
NotContains(String, PartiqlValue),
IsMissing(String),
IsNotMissing(String),
}
#[derive(Debug, Clone)]
pub struct Condition {
pub path: String,
pub op: CompOp,
pub value: PartiqlValue,
}
pub use crate::expressions::condition::CompOp;
#[derive(Debug, Clone, PartialEq)]
pub enum PartiqlValue {
Literal(AttributeValue),
Parameter(usize), }
pub fn parse(input: &str) -> Result<Statement, ParseError> {
let mut tokenizer = Tokenizer::new(input)?;
let first = tokenizer
.next_token()?
.ok_or("Empty statement")?
.to_uppercase();
match first.as_str() {
"SELECT" => parse_select(&mut tokenizer),
"INSERT" => parse_insert(&mut tokenizer),
"UPDATE" => parse_update(&mut tokenizer),
"DELETE" => parse_delete(&mut tokenizer),
_ => Err("Expected data manipulation".into()),
}
}
fn parse_select(t: &mut Tokenizer) -> Result<Statement, ParseError> {
let projections = parse_projections(t)?;
expect_keyword(t, "FROM")?;
let target = parse_table_name(t)?;
let where_clause = parse_optional_where(t)?;
Ok(Statement::Select {
table_name: target.name,
index_name: target.index,
projections,
where_clause,
})
}
fn parse_projections(t: &mut Tokenizer) -> Result<Vec<String>, String> {
let tok = t.peek_token()?.ok_or("Expected projection")?;
if tok == "*" {
t.next_token()?; return Ok(Vec::new());
}
let mut projections = Vec::new();
loop {
let name = t
.next_token()?
.ok_or("Expected projection attribute name")?;
let mut path = unquote(&name);
loop {
match t.peek_token()? {
Some(ref s) if s == "." => {
t.next_token()?; let segment = t.next_token()?.ok_or("Expected attribute name after '.'")?;
path.push('.');
path.push_str(&unquote(&segment));
}
Some(ref s) if s == "[" => {
t.next_token()?; let idx = t.next_token()?.ok_or("Expected index in '[]'")?;
let close = t.next_token()?.ok_or("Expected ']'")?;
if close != "]" {
return Err(format!("Expected ']' but got '{close}'"));
}
path.push('[');
path.push_str(&idx);
path.push(']');
}
_ => break,
}
}
projections.push(path);
match t.peek_token()? {
Some(ref s) if s == "," => {
t.next_token()?; }
_ => break,
}
}
Ok(projections)
}
fn parse_insert(t: &mut Tokenizer) -> Result<Statement, ParseError> {
expect_keyword(t, "INTO")?;
let target = parse_table_name(t)?;
if target.index.is_some() {
return Err(ParseError::Validation(format!(
"FROM clause may only contain a single table name in data \
manipulation statements at {}",
t.position(target.span)
)));
}
let table_name = target.name;
expect_keyword(t, "VALUE")?;
let item = parse_item_literal_partiql(t, 0)?;
let if_not_exists = if let Some(ref tok) = t.peek_token()? {
if tok.eq_ignore_ascii_case("IF") {
t.next_token()?; expect_keyword(t, "NOT")?;
expect_keyword(t, "EXISTS")?;
true
} else {
false
}
} else {
false
};
Ok(Statement::Insert {
table_name,
item,
if_not_exists,
})
}
fn parse_update(t: &mut Tokenizer) -> Result<Statement, ParseError> {
let target = parse_table_name(t)?;
let mut set_clauses = Vec::new();
let mut remove_paths = Vec::new();
if let Some(ref tok) = t.peek_token()?
&& tok.eq_ignore_ascii_case("SET")
{
t.next_token()?; loop {
let path_tok = t.next_token()?.ok_or("Expected attribute path in SET")?;
let path = parse_dotted_path_from_token(&path_tok, t)?;
let eq = t.next_token()?.ok_or("Expected '='")?;
if eq != "=" {
return Err(format!("Expected '=' but got '{eq}'").into());
}
let value = parse_set_value(t)?;
set_clauses.push(SetClause { path, value });
match t.peek_token()? {
Some(ref s) if s == "," => {
t.next_token()?; }
_ => break,
}
}
}
if let Some(ref tok) = t.peek_token()?
&& tok.eq_ignore_ascii_case("REMOVE")
{
t.next_token()?; loop {
let path_tok = t.next_token()?.ok_or("Expected attribute path in REMOVE")?;
let path = parse_dotted_path_from_token(&path_tok, t)?;
remove_paths.push(path);
match t.peek_token()? {
Some(ref s) if s == "," => {
t.next_token()?;
}
_ => break,
}
}
}
if set_clauses.is_empty() && remove_paths.is_empty() {
return Err("UPDATE requires at least one SET or REMOVE clause".into());
}
let where_clause = parse_optional_where(t)?;
let returning = parse_optional_returning(t)?;
Ok(Statement::Update {
table_name: target.name,
index_name: target.index,
set_clauses,
remove_paths,
where_clause,
returning,
})
}
fn parse_set_value(t: &mut Tokenizer) -> Result<SetValue, String> {
if let Some(ref tok) = t.peek_token()?
&& tok.eq_ignore_ascii_case("list_append")
{
t.next_token()?; expect_char(t, "(")?;
let first = parse_value(t, 0)?;
let comma = t.next_token()?.ok_or("Expected ',' in list_append")?;
if comma != "," {
return Err(format!("Expected ',' but got '{comma}'"));
}
let second = parse_value(t, 0)?;
expect_char(t, ")")?;
return Ok(SetValue::ListAppend(first, second));
}
let first = parse_value(t, 0)?;
match t.peek_token()? {
Some(ref s) if s == "+" => {
t.next_token()?; let second = parse_value(t, 0)?;
let attr_path = match &first {
PartiqlValue::Literal(AttributeValue::S(s)) => s.clone(),
_ => {
return Err(
"Expected attribute path on left side of '+' expression".to_string()
);
}
};
Ok(SetValue::Add(attr_path, second))
}
Some(ref s) if s == "-" => {
t.next_token()?; let second = parse_value(t, 0)?;
let attr_path = match &first {
PartiqlValue::Literal(AttributeValue::S(s)) => s.clone(),
_ => {
return Err(
"Expected attribute path on left side of '-' expression".to_string()
);
}
};
Ok(SetValue::Sub(attr_path, second))
}
_ => Ok(SetValue::Simple(first)),
}
}
fn parse_delete(t: &mut Tokenizer) -> Result<Statement, ParseError> {
expect_keyword(t, "FROM")?;
let target = parse_table_name(t)?;
let where_clause = parse_optional_where(t)?;
let returning = parse_optional_returning(t)?;
Ok(Statement::Delete {
table_name: target.name,
index_name: target.index,
where_clause,
returning,
})
}
fn parse_optional_returning(t: &mut Tokenizer) -> Result<Option<ReturningVariant>, String> {
match t.peek_token()? {
Some(ref s) if s.eq_ignore_ascii_case("RETURNING") => {
t.next_token()?; let first = t
.next_token()?
.ok_or("Expected returning variant after RETURNING")?;
let second = t
.next_token()?
.ok_or("Expected returning variant after RETURNING")?;
let star = t.next_token()?.ok_or("Expected '*' in RETURNING clause")?;
if star != "*" || t.peek_token()?.is_some() {
return Err(
"Malformed RETURNING clause; expected RETURNING <ALL|MODIFIED> <OLD|NEW> *"
.to_string(),
);
}
let variant = match (
first.to_ascii_uppercase().as_str(),
second.to_ascii_uppercase().as_str(),
) {
("ALL", "OLD") => ReturningVariant::AllOld,
("MODIFIED", "OLD") => ReturningVariant::ModifiedOld,
("ALL", "NEW") => ReturningVariant::AllNew,
("MODIFIED", "NEW") => ReturningVariant::ModifiedNew,
_ => {
return Err(format!(
"Unsupported RETURNING clause: RETURNING {} {} *",
first.to_ascii_uppercase(),
second.to_ascii_uppercase()
));
}
};
Ok(Some(variant))
}
_ => Ok(None),
}
}
struct TableTarget {
name: String,
index: Option<String>,
span: Span,
}
fn parse_table_name(t: &mut Tokenizer) -> Result<TableTarget, ParseError> {
let span = t.span_at(t.pos);
let raw = t.next_token()?.ok_or("Expected table name")?;
let name = unquote(&raw);
if name.is_empty() {
return Err(ParseError::Validation(
"Path component cannot be an empty string".to_string(),
));
}
let index = match t.peek_token()? {
Some(ref dot) if dot == "." => {
t.next_token()?; let raw = t.next_token()?.ok_or("Expected index name after '.'")?;
let index = unquote(&raw);
if index.is_empty() {
return Err(ParseError::Validation(
"Path component cannot be an empty string".to_string(),
));
}
if matches!(t.peek_token()?, Some(ref dot) if dot == ".") {
return Err(ParseError::Validation(
"A path may contain at most 2 components in the FROM clause".to_string(),
));
}
Some(index)
}
_ => None,
};
Ok(TableTarget { name, index, span })
}
fn parse_optional_where(t: &mut Tokenizer) -> Result<Option<WhereClause>, String> {
match t.peek_token()? {
Some(ref s) if s.eq_ignore_ascii_case("WHERE") => {
t.next_token()?; let (groups, wrote_or) = parse_conditions_with_or(t)?;
Ok(Some(WhereClause::from_groups_written(groups, wrote_or)))
}
_ => Ok(None),
}
}
fn parse_conditions_with_or(t: &mut Tokenizer) -> Result<(Vec<Vec<WhereCondition>>, bool), String> {
let expr = parse_or_expr(t, 0)?;
let wrote_or = contains_or(&expr);
let expr = push_not_down(expr, false)?;
Ok((to_dnf(expr)?, wrote_or))
}
fn contains_or(expr: &BoolExpr) -> bool {
match expr {
BoolExpr::Or(_) => true,
BoolExpr::And(terms) => terms.iter().any(contains_or),
BoolExpr::Not(inner) => contains_or(inner),
BoolExpr::Leaf(_) => false,
}
}
enum BoolExpr {
Leaf(WhereCondition),
And(Vec<BoolExpr>),
Or(Vec<BoolExpr>),
Not(Box<BoolExpr>),
}
fn parse_or_expr(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
let mut terms = vec![parse_and_expr(t, depth)?];
while matches!(t.peek_token()?, Some(ref s) if s.eq_ignore_ascii_case("OR")) {
t.next_token()?;
terms.push(parse_and_expr(t, depth)?);
}
Ok(if terms.len() == 1 {
terms.pop().unwrap()
} else {
BoolExpr::Or(terms)
})
}
fn parse_and_expr(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
let mut terms = vec![parse_not_expr(t, depth)?];
while matches!(t.peek_token()?, Some(ref s) if s.eq_ignore_ascii_case("AND")) {
t.next_token()?;
terms.push(parse_not_expr(t, depth)?);
}
Ok(if terms.len() == 1 {
terms.pop().unwrap()
} else {
BoolExpr::And(terms)
})
}
fn parse_not_expr(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
if matches!(t.peek_token()?, Some(ref s) if s.eq_ignore_ascii_case("NOT")) {
let next = t.peek_token_at(1)?.unwrap_or_default();
let is_condition_form =
next.eq_ignore_ascii_case("EXISTS") || next.eq_ignore_ascii_case("BEGINS_WITH");
if !is_condition_form {
t.next_token()?;
return Ok(BoolExpr::Not(Box::new(parse_not_expr(t, depth + 1)?)));
}
}
parse_primary(t, depth)
}
fn parse_primary(t: &mut Tokenizer, depth: usize) -> Result<BoolExpr, String> {
if matches!(t.peek_token()?, Some(ref s) if s == "(") {
t.next_token()?;
let inner = parse_or_expr(t, depth + 1)?;
match t.next_token()? {
Some(ref s) if s == ")" => Ok(inner),
other => Err(format!(
"Expected ')' but got '{}'",
other.unwrap_or_else(|| "end of statement".to_string())
)),
}
} else {
Ok(BoolExpr::Leaf(parse_single_condition(t, depth)?))
}
}
fn push_not_down(expr: BoolExpr, negated: bool) -> Result<BoolExpr, String> {
match expr {
BoolExpr::Not(inner) => push_not_down(*inner, !negated),
BoolExpr::And(terms) => {
let terms = terms
.into_iter()
.map(|e| push_not_down(e, negated))
.collect::<Result<Vec<_>, _>>()?;
Ok(if negated {
BoolExpr::Or(terms)
} else {
BoolExpr::And(terms)
})
}
BoolExpr::Or(terms) => {
let terms = terms
.into_iter()
.map(|e| push_not_down(e, negated))
.collect::<Result<Vec<_>, _>>()?;
Ok(if negated {
BoolExpr::And(terms)
} else {
BoolExpr::Or(terms)
})
}
BoolExpr::Leaf(cond) => {
if negated {
negate_condition(cond)
} else {
Ok(BoolExpr::Leaf(cond))
}
}
}
}
fn negate_condition(cond: WhereCondition) -> Result<BoolExpr, String> {
use WhereCondition as W;
let not_cmp = |path: String, op: CompOp, value: PartiqlValue| -> BoolExpr {
W::NotComparison(Condition { path, op, value }).into()
};
Ok(match cond {
W::Comparison(c) => W::NotComparison(c).into(),
W::NotComparison(c) => W::Comparison(c).into(),
W::Exists(p) => W::NotExists(p).into(),
W::NotExists(p) => W::Exists(p).into(),
W::BeginsWith(p, v) => W::NotBeginsWith(p, v).into(),
W::NotBeginsWith(p, v) => W::BeginsWith(p, v).into(),
W::IsMissing(p) => W::IsNotMissing(p).into(),
W::IsNotMissing(p) => W::IsMissing(p).into(),
W::Contains(p, v) => W::NotContains(p, v).into(),
W::NotContains(p, v) => W::Contains(p, v).into(),
W::Between(p, low, high) => BoolExpr::Or(vec![
not_cmp(p.clone(), CompOp::Ge, low),
not_cmp(p, CompOp::Le, high),
]),
W::In(p, values) => BoolExpr::And(
values
.into_iter()
.map(|v| not_cmp(p.clone(), CompOp::Eq, v))
.collect(),
),
})
}
impl From<WhereCondition> for BoolExpr {
fn from(cond: WhereCondition) -> Self {
BoolExpr::Leaf(cond)
}
}
const MAX_OR_GROUPS: usize = 256;
const MAX_EXPR_DEPTH: usize = 64;
const MAX_TOTAL_CONDITIONS: usize = 4096;
const TOO_COMPLEX: &str = "WHERE clause is too complex to evaluate";
const TOO_DEEP: &str = "Statement is nested too deeply to parse";
fn to_dnf(expr: BoolExpr) -> Result<Vec<Vec<WhereCondition>>, String> {
let groups = match expr {
BoolExpr::Leaf(cond) => vec![vec![cond]],
BoolExpr::Not(_) => {
unreachable!("push_not_down removes every Not before to_dnf sees the tree")
}
BoolExpr::Or(terms) => {
let mut out = Vec::new();
for term in terms {
out.extend(to_dnf(term)?);
if out.len() > MAX_OR_GROUPS {
return Err(TOO_COMPLEX.to_string());
}
}
out
}
BoolExpr::And(terms) => {
let mut out: Vec<Vec<WhereCondition>> = vec![Vec::new()];
for term in terms {
let term_groups = to_dnf(term)?;
if term_groups.len() == 1 {
for group in out.iter_mut() {
group.extend(term_groups[0].iter().cloned());
}
} else {
if out.len().saturating_mul(term_groups.len()) > MAX_OR_GROUPS {
return Err(TOO_COMPLEX.to_string());
}
let mut next = Vec::with_capacity(out.len() * term_groups.len());
for existing in &out {
for group in &term_groups {
let mut combined = existing.clone();
combined.extend(group.iter().cloned());
next.push(combined);
}
}
out = next;
}
if out.iter().map(Vec::len).sum::<usize>() > MAX_TOTAL_CONDITIONS {
return Err(TOO_COMPLEX.to_string());
}
}
out
}
};
if groups.len() > MAX_OR_GROUPS {
return Err(TOO_COMPLEX.to_string());
}
Ok(groups)
}
fn parse_single_condition(t: &mut Tokenizer, depth: usize) -> Result<WhereCondition, String> {
let tok = t.next_token()?.ok_or("Expected condition in WHERE")?;
let tok_upper = tok.to_uppercase();
match tok_upper.as_str() {
"EXISTS" => {
expect_char(t, "(")?;
let path = parse_function_path(t)?;
expect_char(t, ")")?;
Ok(WhereCondition::Exists(path))
}
"BEGINS_WITH" => {
expect_char(t, "(")?;
let path = parse_function_path(t)?;
let comma = t.next_token()?.ok_or("Expected ',' in BEGINS_WITH")?;
if comma != "," {
return Err(format!("Expected ',' but got '{comma}'"));
}
let value = parse_value(t, depth)?;
expect_char(t, ")")?;
Ok(WhereCondition::BeginsWith(path, value))
}
"CONTAINS" => {
expect_char(t, "(")?;
let path = parse_function_path(t)?;
let comma = t.next_token()?.ok_or("Expected ',' in CONTAINS")?;
if comma != "," {
return Err(format!("Expected ',' but got '{comma}'"));
}
let value = parse_value(t, depth)?;
expect_char(t, ")")?;
Ok(WhereCondition::Contains(path, value))
}
"NOT" => {
let func = t.next_token()?.ok_or("Expected function name after NOT")?;
if func.eq_ignore_ascii_case("EXISTS") {
expect_char(t, "(")?;
let path = parse_function_path(t)?;
expect_char(t, ")")?;
Ok(WhereCondition::NotExists(path))
} else if func.eq_ignore_ascii_case("BEGINS_WITH") {
expect_char(t, "(")?;
let path = parse_function_path(t)?;
let comma = t.next_token()?.ok_or("Expected ',' in NOT BEGINS_WITH")?;
if comma != "," {
return Err(format!("Expected ',' but got '{comma}'"));
}
let value = parse_value(t, depth)?;
expect_char(t, ")")?;
Ok(WhereCondition::NotBeginsWith(path, value))
} else {
Err(format!("Unsupported NOT function: {func}"))
}
}
_ => {
let path = parse_dotted_path_from_token(&tok, t)?;
let next = t
.peek_token()?
.ok_or("Expected operator after attribute path")?;
let next_upper = next.to_uppercase();
match next_upper.as_str() {
"BETWEEN" => {
t.next_token()?; let low = parse_value(t, depth)?;
expect_keyword(t, "AND")?;
let high = parse_value(t, depth)?;
Ok(WhereCondition::Between(path, low, high))
}
"IN" => {
t.next_token()?; let open = t.next_token()?.ok_or("Expected '(' or '[' after IN")?;
let close_char = match open.as_str() {
"(" => ")",
"[" => "]",
other => {
return Err(format!("Expected '(' or '[' after IN, got '{other}'"));
}
};
let mut values = Vec::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of IN list")?;
if peek == close_char {
t.next_token()?; break;
}
if peek == "," {
t.next_token()?; continue;
}
values.push(parse_value(t, depth)?);
}
Ok(WhereCondition::In(path, values))
}
"IS" => {
t.next_token()?; let kw = t.next_token()?.ok_or("Expected MISSING or NOT after IS")?;
let kw_upper = kw.to_uppercase();
match kw_upper.as_str() {
"MISSING" => Ok(WhereCondition::IsMissing(path)),
"NOT" => {
expect_keyword(t, "MISSING")?;
Ok(WhereCondition::IsNotMissing(path))
}
other => Err(format!(
"Expected MISSING or NOT MISSING after IS, got '{other}'"
)),
}
}
_ => {
let op_tok = t.next_token()?.ok_or("Expected comparison operator")?;
let op = match op_tok.as_str() {
"=" => CompOp::Eq,
"<>" | "!=" => CompOp::Ne,
"<" => CompOp::Lt,
"<=" => CompOp::Le,
">" => CompOp::Gt,
">=" => CompOp::Ge,
other => return Err(format!("Unknown operator: {other}")),
};
let value = parse_value(t, depth)?;
Ok(WhereCondition::Comparison(Condition { path, op, value }))
}
}
}
}
}
fn parse_dotted_path_from_token(first_tok: &str, t: &mut Tokenizer) -> Result<String, String> {
let mut path = unquote(first_tok);
while let Some(ref next) = t.peek_token()? {
if next == "." {
t.next_token()?; let seg = t.next_token()?.ok_or("Expected attribute name after '.'")?;
path.push('.');
path.push_str(&unquote(&seg));
} else if next == "[" {
t.next_token()?; let idx = t.next_token()?.ok_or("Expected index in '[]'")?;
let close = t.next_token()?.ok_or("Expected ']'")?;
if close != "]" {
return Err(format!("Expected ']' but got '{close}'"));
}
path.push('[');
path.push_str(&idx);
path.push(']');
} else {
break;
}
}
Ok(path)
}
fn parse_function_path(t: &mut Tokenizer) -> Result<String, String> {
let tok = t.next_token()?.ok_or("Expected path in function")?;
parse_dotted_path_from_token(&tok, t)
}
fn expect_char(t: &mut Tokenizer, expected: &str) -> Result<(), String> {
let tok = t.next_token()?.ok_or(format!("Expected '{expected}'"))?;
if tok != expected {
return Err(format!("Expected '{expected}' but got '{tok}'"));
}
Ok(())
}
fn parse_value(t: &mut Tokenizer, depth: usize) -> Result<PartiqlValue, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
let tok = t.next_token()?.ok_or("Expected value")?;
if tok == "?" {
let idx = t.next_param_index();
return Ok(PartiqlValue::Parameter(idx));
}
if tok.starts_with('\'') && tok.ends_with('\'') && tok.len() >= 2 {
let s = tok[1..tok.len() - 1].to_string();
return Ok(PartiqlValue::Literal(AttributeValue::S(s)));
}
if tok == "<"
&& let Some(ref next) = t.peek_token()?
&& next == "<"
{
t.next_token()?; let mut elements = Vec::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of set literal")?;
if peek == ">" {
t.next_token()?; let next_close = t.peek_token()?;
if next_close.as_deref() == Some(">") {
t.next_token()?;
}
break;
}
if peek == "," {
t.next_token()?;
continue;
}
elements.push(parse_value(t, depth + 1)?);
}
return set_literal_to_value(elements);
}
if tok == "[" {
let mut items = Vec::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of list")?;
if peek == "]" {
t.next_token()?;
break;
}
if peek == "," {
t.next_token()?;
continue;
}
items.push(parse_value(t, depth + 1)?);
}
let mut avs = Vec::new();
for item in items {
match item {
PartiqlValue::Literal(av) => avs.push(av),
PartiqlValue::Parameter(_) => {
return Err(
"Parameter placeholders inside list literals are not yet supported"
.to_string(),
);
}
}
}
return Ok(PartiqlValue::Literal(AttributeValue::L(avs)));
}
if tok == "{" {
let mut map = HashMap::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of map literal")?;
if peek == "}" {
t.next_token()?;
break;
}
if peek == "," {
t.next_token()?;
continue;
}
let key_tok = t.next_token()?.ok_or("Expected key in map literal")?;
let key = unquote(&key_tok);
let colon = t.next_token()?.ok_or("Expected ':'")?;
if colon != ":" {
return Err(format!("Expected ':' but got '{colon}'"));
}
let val = parse_value(t, depth + 1)?;
match val {
PartiqlValue::Literal(av) => {
map.insert(key, av);
}
PartiqlValue::Parameter(_) => {
return Err(
"Parameter placeholders inside map literals are not yet supported"
.to_string(),
);
}
}
}
return Ok(PartiqlValue::Literal(AttributeValue::M(map)));
}
if (tok == "-" || tok == "+")
&& let Some(ref next) = t.peek_token()?
&& next.starts_with(|c: char| c.is_ascii_digit())
{
let num = t.next_token()?.unwrap();
return Ok(PartiqlValue::Literal(AttributeValue::N(format!(
"{tok}{num}"
))));
}
if tok.starts_with(|c: char| c.is_ascii_digit()) {
return Ok(PartiqlValue::Literal(AttributeValue::N(tok)));
}
match tok.to_uppercase().as_str() {
"TRUE" => return Ok(PartiqlValue::Literal(AttributeValue::BOOL(true))),
"FALSE" => return Ok(PartiqlValue::Literal(AttributeValue::BOOL(false))),
"NULL" => return Ok(PartiqlValue::Literal(AttributeValue::NULL(true))),
_ => {}
}
if tok
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
{
let mut path = tok.clone();
while let Some(ref next) = t.peek_token()? {
if next == "." {
t.next_token()?;
let seg = t.next_token()?.ok_or("Expected attribute name after '.'")?;
path.push('.');
path.push_str(&unquote(&seg));
} else {
break;
}
}
return Ok(PartiqlValue::Literal(AttributeValue::S(path)));
}
Err(format!("Unexpected value token: {tok}"))
}
fn set_literal_to_value(elements: Vec<PartiqlValue>) -> Result<PartiqlValue, String> {
if elements.is_empty() {
return Err("Set literals cannot be empty".to_string());
}
let first = match &elements[0] {
PartiqlValue::Literal(av) => av,
PartiqlValue::Parameter(_) => {
return Err("Parameter placeholders in set literals are not supported".to_string());
}
};
match first {
AttributeValue::S(_) => {
let mut ss = Vec::new();
for elem in &elements {
match elem {
PartiqlValue::Literal(AttributeValue::S(s)) => ss.push(s.clone()),
_ => return Err("Mixed types in string set literal".to_string()),
}
}
Ok(PartiqlValue::Literal(AttributeValue::SS(ss)))
}
AttributeValue::N(_) => {
let mut ns = Vec::new();
for elem in &elements {
match elem {
PartiqlValue::Literal(AttributeValue::N(n)) => ns.push(n.clone()),
_ => return Err("Mixed types in number set literal".to_string()),
}
}
Ok(PartiqlValue::Literal(AttributeValue::NS(ns)))
}
_ => Err(format!(
"Unsupported element type in set literal: {first:?}"
)),
}
}
fn parse_item_literal(
t: &mut Tokenizer,
depth: usize,
) -> Result<HashMap<String, AttributeValue>, String> {
let open = t.next_token()?.ok_or("Expected '{'")?;
if open != "{" {
return Err(format!("Expected '{{' but got '{open}'"));
}
let mut item = HashMap::new();
loop {
let tok = t.peek_token()?.ok_or("Unexpected end of item literal")?;
if tok == "}" {
t.next_token()?; break;
}
if tok == "," {
t.next_token()?;
continue;
}
let key_tok = t.next_token()?.ok_or("Expected key in item literal")?;
let key = unquote(&key_tok);
let colon = t.next_token()?.ok_or("Expected ':'")?;
if colon != ":" {
return Err(format!("Expected ':' but got '{colon}'"));
}
let val = parse_item_value(t, depth)?;
item.insert(key, val);
}
Ok(item)
}
fn parse_item_value(t: &mut Tokenizer, depth: usize) -> Result<AttributeValue, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
let tok = t.peek_token()?.ok_or("Expected value")?;
if tok == "{" {
let inner = parse_item_literal(t, depth + 1)?;
return Ok(AttributeValue::M(inner));
}
if tok == "[" {
t.next_token()?; let mut items = Vec::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of list")?;
if peek == "]" {
t.next_token()?;
break;
}
if peek == "," {
t.next_token()?;
continue;
}
items.push(parse_item_value(t, depth + 1)?);
}
return Ok(AttributeValue::L(items));
}
if tok == "<"
&& let Some(ref next_tok) = t.peek_token_at(1)?
&& next_tok == "<"
{
t.next_token()?; t.next_token()?; let mut elements = Vec::new();
loop {
let peek = t.peek_token()?.ok_or("Unexpected end of set literal")?;
if peek == ">" {
t.next_token()?; if t.peek_token()?.as_deref() == Some(">") {
t.next_token()?; }
break;
}
if peek == "," {
t.next_token()?;
continue;
}
elements.push(parse_item_value(t, depth + 1)?);
}
return item_value_set_literal(elements);
}
let tok = t.next_token()?.ok_or("Expected value")?;
if tok.starts_with('\'') && tok.ends_with('\'') && tok.len() >= 2 {
return Ok(AttributeValue::S(tok[1..tok.len() - 1].to_string()));
}
if (tok == "-" || tok == "+")
&& let Some(ref next) = t.peek_token()?
&& next.starts_with(|c: char| c.is_ascii_digit())
{
let num = t.next_token()?.unwrap();
return Ok(AttributeValue::N(format!("{tok}{num}")));
}
if tok.starts_with(|c: char| c.is_ascii_digit()) {
return Ok(AttributeValue::N(tok));
}
match tok.to_uppercase().as_str() {
"TRUE" => Ok(AttributeValue::BOOL(true)),
"FALSE" => Ok(AttributeValue::BOOL(false)),
"NULL" => Ok(AttributeValue::NULL(true)),
_ => Err(format!("Unexpected value in item literal: {tok}")),
}
}
fn item_value_set_literal(elements: Vec<AttributeValue>) -> Result<AttributeValue, String> {
if elements.is_empty() {
return Err("Set literals cannot be empty".to_string());
}
match &elements[0] {
AttributeValue::S(_) => {
let mut ss = Vec::new();
for e in elements {
match e {
AttributeValue::S(s) => ss.push(s),
_ => return Err("Mixed types in string set literal".to_string()),
}
}
Ok(AttributeValue::SS(ss))
}
AttributeValue::N(_) => {
let mut ns = Vec::new();
for e in elements {
match e {
AttributeValue::N(n) => ns.push(n),
_ => return Err("Mixed types in number set literal".to_string()),
}
}
Ok(AttributeValue::NS(ns))
}
_ => Err(format!(
"Unsupported element type in set literal: {:?}",
elements[0]
)),
}
}
fn parse_item_literal_partiql(
t: &mut Tokenizer,
depth: usize,
) -> Result<HashMap<String, PartiqlValue>, String> {
let open = t.next_token()?.ok_or("Expected '{'")?;
if open != "{" {
return Err(format!("Expected '{{' but got '{open}'"));
}
let mut item = HashMap::new();
loop {
let tok = t.peek_token()?.ok_or("Unexpected end of item literal")?;
if tok == "}" {
t.next_token()?; break;
}
if tok == "," {
t.next_token()?;
continue;
}
let key_tok = t.next_token()?.ok_or("Expected key in item literal")?;
let key = unquote(&key_tok);
let colon = t.next_token()?.ok_or("Expected ':'")?;
if colon != ":" {
return Err(format!("Expected ':' but got '{colon}'"));
}
let val = parse_item_value_partiql(t, depth)?;
item.insert(key, val);
}
Ok(item)
}
fn parse_item_value_partiql(t: &mut Tokenizer, depth: usize) -> Result<PartiqlValue, String> {
if depth > MAX_EXPR_DEPTH {
return Err(TOO_DEEP.to_string());
}
let tok = t.peek_token()?.ok_or("Expected value")?;
if tok == "?" {
t.next_token()?; let idx = t.next_param_index();
return Ok(PartiqlValue::Parameter(idx));
}
if tok == "[" {
return parse_value(t, depth + 1);
}
if tok == "{" {
let inner = parse_item_literal_partiql(t, depth + 1)?;
let mut map = HashMap::new();
for (k, v) in inner {
match v {
PartiqlValue::Literal(av) => {
map.insert(k, av);
}
PartiqlValue::Parameter(_) => {
return Err(
"Parameter placeholders inside nested map literals are not yet fully supported"
.to_string(),
);
}
}
}
return Ok(PartiqlValue::Literal(AttributeValue::M(map)));
}
let av = parse_item_value(t, depth)?;
Ok(PartiqlValue::Literal(av))
}
fn unquote(s: &str) -> String {
if s.len() >= 2
&& ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
{
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}
fn expect_keyword(t: &mut Tokenizer, kw: &str) -> Result<(), String> {
let tok = t.next_token()?.ok_or(format!("Expected '{kw}'"))?;
if !tok.eq_ignore_ascii_case(kw) {
return Err(format!("Expected '{kw}' but got '{tok}'"));
}
Ok(())
}
struct Tokenizer {
tokens: Vec<String>,
spans: Vec<Span>,
input: String,
pos: usize,
param_counter: usize,
}
impl Tokenizer {
fn new(input: &str) -> Result<Self, String> {
let (tokens, spans) = tokenize(input)?;
Ok(Self {
tokens,
spans,
input: input.to_string(),
pos: 0,
param_counter: 0,
})
}
fn span_at(&self, index: usize) -> Span {
self.spans
.get(index)
.copied()
.unwrap_or(Span { start: 0, len: 0 })
}
fn position(&self, span: Span) -> String {
let (line, col) = span.line_col(&self.input);
format!("{line}:{col}:{}", span.len)
}
fn next_token(&mut self) -> Result<Option<String>, String> {
if self.pos >= self.tokens.len() {
return Ok(None);
}
let tok = self.tokens[self.pos].clone();
self.pos += 1;
Ok(Some(tok))
}
fn peek_token(&self) -> Result<Option<String>, String> {
if self.pos >= self.tokens.len() {
return Ok(None);
}
Ok(Some(self.tokens[self.pos].clone()))
}
fn peek_token_at(&self, offset: usize) -> Result<Option<String>, String> {
let idx = self.pos + offset;
if idx >= self.tokens.len() {
return Ok(None);
}
Ok(Some(self.tokens[idx].clone()))
}
fn next_param_index(&mut self) -> usize {
let idx = self.param_counter;
self.param_counter += 1;
idx
}
}
#[derive(Debug, Clone, Copy)]
struct Span {
start: usize,
len: usize,
}
impl Span {
fn line_col(&self, input: &str) -> (usize, usize) {
let (mut line, mut col) = (1usize, 1usize);
for c in input.chars().take(self.start) {
if c == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
}
fn emit(tokens: &mut Vec<String>, spans: &mut Vec<Span>, tok: String, start: usize, end: usize) {
spans.push(Span {
start,
len: end - start,
});
tokens.push(tok);
}
fn tokenize(input: &str) -> Result<(Vec<String>, Vec<Span>), String> {
let chars: Vec<char> = input.chars().collect();
let len = chars.len();
let mut tokens = Vec::new();
let mut spans = Vec::with_capacity(len / 4 + 1);
let mut i = 0;
while i < len {
if chars[i].is_ascii_whitespace() {
i += 1;
continue;
}
let start = i;
match chars[i] {
'{' | '}' | '[' | ']' | '(' | ')' | ',' | ':' | '*' | '?' | '+' | '-' | '.' => {
let tok = chars[i].to_string();
i += 1;
emit(&mut tokens, &mut spans, tok, start, i);
continue;
}
_ => {}
}
if i + 1 < len {
let two = format!("{}{}", chars[i], chars[i + 1]);
match two.as_str() {
"<>" | "<=" | ">=" | "!=" => {
i += 2;
emit(&mut tokens, &mut spans, two, start, i);
continue;
}
_ => {}
}
}
if matches!(chars[i], '=' | '<' | '>') {
let tok = chars[i].to_string();
i += 1;
emit(&mut tokens, &mut spans, tok, start, i);
continue;
}
if chars[i] == '\'' {
let mut s = String::from('\'');
i += 1;
while i < len {
if chars[i] == '\'' {
if i + 1 < len && chars[i + 1] == '\'' {
s.push('\'');
i += 2;
} else {
break; }
} else {
s.push(chars[i]);
i += 1;
}
}
if i >= len {
return Err("Unterminated string literal".to_string());
}
s.push('\'');
i += 1;
emit(&mut tokens, &mut spans, s, start, i);
continue;
}
if chars[i] == '"' {
let mut s = String::from('"');
i += 1;
while i < len {
if chars[i] == '"' {
if i + 1 < len && chars[i + 1] == '"' {
s.push('"');
i += 2;
} else {
break; }
} else {
s.push(chars[i]);
i += 1;
}
}
if i >= len {
return Err("Unterminated quoted identifier".to_string());
}
s.push('"');
i += 1;
emit(&mut tokens, &mut spans, s, start, i);
continue;
}
if chars[i].is_ascii_digit() {
let mut s = String::new();
while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') {
s.push(chars[i]);
i += 1;
}
emit(&mut tokens, &mut spans, s, start, i);
continue;
}
if chars[i].is_ascii_alphabetic() || chars[i] == '_' {
let mut s = String::new();
while i < len && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
s.push(chars[i]);
i += 1;
}
emit(&mut tokens, &mut spans, s, start, i);
continue;
}
return Err(format!("Unexpected character: '{}'", chars[i]));
}
Ok((tokens, spans))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_select_star() {
let stmt = parse("SELECT * FROM \"TestTable\"").unwrap();
match stmt {
Statement::Select {
table_name,
projections,
where_clause,
..
} => {
assert_eq!(table_name, "TestTable");
assert!(projections.is_empty());
assert!(where_clause.is_none());
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_select_with_where() {
let stmt = parse("SELECT * FROM \"T\" WHERE pk = 'hello'").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &wc.groups[0][0] {
WhereCondition::Comparison(c) => {
assert_eq!(c.path, "pk");
assert_eq!(c.op, CompOp::Eq);
}
_ => panic!("Expected Comparison"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_select_with_projection() {
let stmt = parse("SELECT name, age FROM \"Users\"").unwrap();
match stmt {
Statement::Select { projections, .. } => {
assert_eq!(projections, vec!["name", "age"]);
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_insert() {
let stmt =
parse("INSERT INTO \"TestTable\" VALUE {'pk': 'key1', 'data': 'hello'}").unwrap();
match stmt {
Statement::Insert {
table_name, item, ..
} => {
assert_eq!(table_name, "TestTable");
assert_eq!(
item.get("pk"),
Some(&PartiqlValue::Literal(AttributeValue::S(
"key1".to_string()
)))
);
assert_eq!(
item.get("data"),
Some(&PartiqlValue::Literal(AttributeValue::S(
"hello".to_string()
)))
);
}
_ => panic!("Expected INSERT"),
}
}
#[test]
fn test_parse_update() {
let stmt = parse("UPDATE \"T\" SET name = 'Bob' WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Update {
table_name,
set_clauses,
where_clause,
..
} => {
assert_eq!(table_name, "T");
assert_eq!(set_clauses.len(), 1);
assert_eq!(set_clauses[0].path, "name");
assert!(where_clause.is_some());
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn test_parse_delete() {
let stmt = parse("DELETE FROM \"T\" WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Delete {
table_name,
where_clause,
returning,
..
} => {
assert_eq!(table_name, "T");
assert!(where_clause.is_some());
assert!(returning.is_none());
}
_ => panic!("Expected DELETE"),
}
}
#[test]
fn test_parse_delete_returning_all_old() {
let stmt = parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING ALL OLD *").unwrap();
match stmt {
Statement::Delete {
table_name,
where_clause,
returning,
..
} => {
assert_eq!(table_name, "T");
assert!(where_clause.is_some());
assert_eq!(returning, Some(ReturningVariant::AllOld));
}
_ => panic!("Expected DELETE"),
}
}
#[test]
fn test_parse_delete_returning_is_case_insensitive() {
let stmt = parse("DELETE FROM \"T\" WHERE pk = 'k1' returning all old *").unwrap();
match stmt {
Statement::Delete { returning, .. } => {
assert_eq!(returning, Some(ReturningVariant::AllOld))
}
_ => panic!("Expected DELETE"),
}
}
#[test]
fn test_parse_delete_returning_recognises_all_variants() {
let cases = [
("RETURNING ALL OLD *", ReturningVariant::AllOld),
("RETURNING MODIFIED OLD *", ReturningVariant::ModifiedOld),
("RETURNING ALL NEW *", ReturningVariant::AllNew),
("RETURNING MODIFIED NEW *", ReturningVariant::ModifiedNew),
];
for (clause, expected) in cases {
let stmt = parse(&format!("DELETE FROM \"T\" WHERE pk = 'k1' {clause}")).unwrap();
match stmt {
Statement::Delete { returning, .. } => {
assert_eq!(returning, Some(expected), "clause {clause}")
}
_ => panic!("Expected DELETE for {clause}"),
}
}
}
#[test]
fn test_parse_update_returning_recognises_all_variants() {
let cases = [
("RETURNING ALL OLD *", ReturningVariant::AllOld),
("RETURNING MODIFIED OLD *", ReturningVariant::ModifiedOld),
("RETURNING ALL NEW *", ReturningVariant::AllNew),
("RETURNING MODIFIED NEW *", ReturningVariant::ModifiedNew),
];
for (clause, expected) in cases {
let stmt = parse(&format!(
"UPDATE \"T\" SET data = 'x' WHERE pk = 'k1' {clause}"
))
.unwrap();
match stmt {
Statement::Update { returning, .. } => {
assert_eq!(returning, Some(expected), "clause {clause}")
}
_ => panic!("Expected UPDATE for {clause}"),
}
}
}
#[test]
fn test_parse_delete_returning_rejects_incomplete_clause() {
assert!(parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING ALL OLD").is_err());
assert!(parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING").is_err());
assert!(parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING ALL OLD attr").is_err());
}
#[test]
fn test_parse_returning_rejects_invalid_keyword_pairs() {
for clause in [
"RETURNING OLD ALL *",
"RETURNING NEW MODIFIED *",
"RETURNING ALL ALL *",
"RETURNING FOO BAR *",
] {
let err = parse(&format!("DELETE FROM \"T\" WHERE pk = 'k1' {clause}")).unwrap_err();
assert!(
err.to_string().contains("Unsupported RETURNING clause"),
"clause {clause} gave unexpected error: {err}"
);
}
}
#[test]
fn test_parse_parameter() {
let stmt = parse("SELECT * FROM \"T\" WHERE pk = ?").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => match &wc.groups[0][0] {
WhereCondition::Comparison(c) => match &c.value {
PartiqlValue::Parameter(0) => {}
other => panic!("Expected Parameter(0), got {other:?}"),
},
_ => panic!("Expected Comparison"),
},
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_numeric_literal() {
let stmt = parse("SELECT * FROM \"T\" WHERE age > 42").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => match &wc.groups[0][0] {
WhereCondition::Comparison(c) => {
assert_eq!(c.op, CompOp::Gt);
match &c.value {
PartiqlValue::Literal(AttributeValue::N(n)) => assert_eq!(n, "42"),
other => panic!("Expected N(42), got {other:?}"),
}
}
_ => panic!("Expected Comparison"),
},
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_insert_with_number() {
let stmt = parse("INSERT INTO \"T\" VALUE {'pk': 'k1', 'age': 25}").unwrap();
match stmt {
Statement::Insert { item, .. } => {
assert_eq!(
item.get("age"),
Some(&PartiqlValue::Literal(AttributeValue::N("25".to_string())))
);
}
_ => panic!("Expected INSERT"),
}
}
#[test]
fn test_invalid_statement() {
let result = parse("MERGE INTO \"T\"");
assert!(result.is_err());
}
#[test]
fn test_empty_statement() {
let result = parse("");
assert!(result.is_err());
}
#[test]
fn test_parse_between() {
let stmt = parse("SELECT * FROM \"T\" WHERE age BETWEEN 18 AND 65").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &wc.groups[0][0] {
WhereCondition::Between(path, low, high) => {
assert_eq!(path, "age");
match low {
PartiqlValue::Literal(AttributeValue::N(n)) => assert_eq!(n, "18"),
other => panic!("Expected N(18), got {other:?}"),
}
match high {
PartiqlValue::Literal(AttributeValue::N(n)) => assert_eq!(n, "65"),
other => panic!("Expected N(65), got {other:?}"),
}
}
other => panic!("Expected Between, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_between_and_other_condition() {
let stmt = parse("SELECT * FROM \"T\" WHERE x BETWEEN 1 AND 10 AND y = 'hello'").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 2);
assert!(matches!(&wc.groups[0][0], WhereCondition::Between(..)));
assert!(matches!(&wc.groups[0][1], WhereCondition::Comparison(..)));
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_in() {
let stmt = parse("SELECT * FROM \"T\" WHERE status IN ('ACTIVE', 'PENDING')").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &wc.groups[0][0] {
WhereCondition::In(path, values) => {
assert_eq!(path, "status");
assert_eq!(values.len(), 2);
}
other => panic!("Expected In, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_contains() {
let stmt = parse("SELECT * FROM \"T\" WHERE CONTAINS(name, 'john')").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &wc.groups[0][0] {
WhereCondition::Contains(path, val) => {
assert_eq!(path, "name");
match val {
PartiqlValue::Literal(AttributeValue::S(s)) => {
assert_eq!(s, "john")
}
other => panic!("Expected S(john), got {other:?}"),
}
}
other => panic!("Expected Contains, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_is_missing() {
let stmt = parse("SELECT * FROM \"T\" WHERE email IS MISSING").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &wc.groups[0][0] {
WhereCondition::IsMissing(path) => assert_eq!(path, "email"),
other => panic!("Expected IsMissing, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_is_not_missing() {
let stmt = parse("SELECT * FROM \"T\" WHERE email IS NOT MISSING").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups[0].len(), 1);
match &wc.groups[0][0] {
WhereCondition::IsNotMissing(path) => assert_eq!(path, "email"),
other => panic!("Expected IsNotMissing, got {other:?}"),
}
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_nested_projection() {
let stmt = parse("SELECT a.b.c, d FROM \"T\"").unwrap();
match stmt {
Statement::Select { projections, .. } => {
assert_eq!(projections, vec!["a.b.c", "d"]);
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_array_index_projection() {
let stmt = parse("SELECT items[0].name FROM \"T\"").unwrap();
match stmt {
Statement::Select { projections, .. } => {
assert_eq!(projections, vec!["items[0].name"]);
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn test_parse_update_with_remove() {
let stmt =
parse("UPDATE \"T\" SET name = 'Bob' REMOVE age, email WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Update {
set_clauses,
remove_paths,
where_clause,
..
} => {
assert_eq!(set_clauses.len(), 1);
assert_eq!(remove_paths, vec!["age", "email"]);
assert!(where_clause.is_some());
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn test_parse_update_remove_only() {
let stmt = parse("UPDATE \"T\" REMOVE old_field WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Update {
set_clauses,
remove_paths,
..
} => {
assert!(set_clauses.is_empty());
assert_eq!(remove_paths, vec!["old_field"]);
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn test_parse_set_expression_add() {
let stmt = parse("UPDATE \"T\" SET count = count + 1 WHERE pk = 'k1'").unwrap();
match stmt {
Statement::Update { set_clauses, .. } => {
assert_eq!(set_clauses.len(), 1);
match &set_clauses[0].value {
SetValue::Add(attr, val) => {
assert_eq!(attr, "count");
assert_eq!(
val,
&PartiqlValue::Literal(AttributeValue::N("1".to_string()))
);
}
other => panic!("Expected Add, got {other:?}"),
}
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn test_parse_count_star_is_a_parse_error() {
assert!(parse("SELECT COUNT(*) FROM \"T\"").is_err());
}
#[test]
fn test_count_rejection_reports_the_token_position() {
assert_eq!(
count_projection_rejection("SELECT COUNT(*) FROM \"T\""),
Some("Unexpected path component at 1:8:5".to_string())
);
assert_eq!(
count_projection_rejection("SELECT count(pk) FROM \"T\""),
Some("Unexpected path component at 1:9:5".to_string())
);
}
#[test]
fn test_count_rejection_tracks_lines() {
assert_eq!(
count_projection_rejection("SELECT\n COUNT(*) FROM \"T\""),
Some("Unexpected path component at 2:2:5".to_string())
);
}
#[test]
fn test_count_rejection_ignores_quotes_and_word_boundaries() {
assert_eq!(
count_projection_rejection("SELECT \"COUNT\" FROM \"T\""),
None
);
assert_eq!(
count_projection_rejection("SELECT 'count(' FROM \"T\""),
None
);
assert_eq!(
count_projection_rejection("SELECT recount(x) FROM \"T\""),
None
);
}
#[test]
fn test_count_rejection_only_applies_before_from_on_a_select() {
assert_eq!(
count_projection_rejection("UPDATE \"T\" SET count = count + 1 WHERE pk = 'k1'"),
None
);
assert_eq!(
count_projection_rejection("SELECT pk FROM \"T\" WHERE count(x)"),
None
);
}
#[test]
fn test_parse_set_literal() {
let stmt = parse("INSERT INTO \"T\" VALUE {'pk': 'k1', 'tags': <<'a', 'b'>>}").unwrap();
match stmt {
Statement::Insert { item, .. } => match item.get("tags") {
Some(PartiqlValue::Literal(AttributeValue::SS(ss))) => {
assert!(ss.contains(&"a".to_string()));
assert!(ss.contains(&"b".to_string()));
}
other => panic!("Expected SS, got {other:?}"),
},
_ => panic!("Expected INSERT"),
}
}
#[test]
fn test_parse_or_condition() {
let stmt = parse("SELECT * FROM \"T\" WHERE status = 'A' OR status = 'B'").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups.len(), 2);
assert_eq!(wc.groups[0].len(), 1);
assert_eq!(wc.groups[1].len(), 1);
}
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_and_or_mixed() {
let stmt = parse("SELECT * FROM \"T\" WHERE a = 1 AND b = 2 OR c = 3").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => {
assert_eq!(wc.groups.len(), 2);
assert_eq!(wc.groups[0].len(), 2); assert_eq!(wc.groups[1].len(), 1); }
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn test_parse_insert_if_not_exists() {
let stmt =
parse("INSERT INTO \"T\" VALUE {'pk': 'k1', 'name': 'A'} IF NOT EXISTS").unwrap();
match stmt {
Statement::Insert { if_not_exists, .. } => {
assert!(if_not_exists);
}
_ => panic!("Expected INSERT"),
}
}
#[test]
fn test_parse_nested_path_in_where_function() {
let stmt = parse("SELECT * FROM \"T\" WHERE BEGINS_WITH(address.city, 'Lon')").unwrap();
match stmt {
Statement::Select {
where_clause: Some(wc),
..
} => match &wc.groups[0][0] {
WhereCondition::BeginsWith(path, _) => {
assert_eq!(path, "address.city");
}
other => panic!("Expected BeginsWith, got {other:?}"),
},
_ => panic!("Expected SELECT with WHERE"),
}
}
#[test]
fn select_index_qualifier_keeps_the_where_clause() {
let stmt = parse("SELECT * FROM \"T\".\"idx\" WHERE pk = 'p'").unwrap();
match stmt {
Statement::Select {
table_name,
where_clause,
..
} => {
assert_eq!(table_name, "T");
assert!(
where_clause.is_some(),
"the qualifier stranded the WHERE clause"
);
}
_ => panic!("Expected SELECT"),
}
}
#[test]
fn delete_index_qualifier_keeps_the_where_clause() {
let stmt = parse("DELETE FROM \"T\".\"idx\" WHERE pk = 'p'").unwrap();
match stmt {
Statement::Delete {
table_name,
where_clause,
..
} => {
assert_eq!(table_name, "T");
assert!(
where_clause.is_some(),
"the qualifier stranded the WHERE clause"
);
}
_ => panic!("Expected DELETE"),
}
}
#[test]
fn update_index_qualifier_keeps_its_clauses() {
let stmt = parse("UPDATE \"T\".\"idx\" SET a = 'x' WHERE pk = 'p'")
.expect("the qualifier stranded the SET clause");
match stmt {
Statement::Update {
table_name,
set_clauses,
where_clause,
..
} => {
assert_eq!(table_name, "T");
assert_eq!(set_clauses.len(), 1);
assert!(where_clause.is_some());
}
_ => panic!("Expected UPDATE"),
}
}
#[test]
fn select_index_qualifier_is_captured() {
let stmt = parse("SELECT * FROM \"T\".\"idx\"").unwrap();
assert_eq!(index_name(&stmt), Some("idx"));
assert_eq!(table_name(&stmt), Some("T"));
}
#[test]
fn select_without_a_qualifier_carries_no_index() {
let stmt = parse("SELECT * FROM \"T\" WHERE pk = 'p'").unwrap();
assert_eq!(index_name(&stmt), None);
}
#[test]
fn unquoted_index_qualifier_parses() {
let stmt = parse("SELECT * FROM T.idx").unwrap();
assert_eq!(table_name(&stmt), Some("T"));
assert_eq!(index_name(&stmt), Some("idx"));
}
#[test]
fn write_statements_carry_the_qualifier_for_the_executor_to_reject() {
let stmt = parse("UPDATE \"T\".\"idx\" SET a = 'x' WHERE pk = 'p'").unwrap();
assert_eq!(index_name(&stmt), Some("idx"));
let stmt = parse("DELETE FROM \"T\".\"idx\" WHERE pk = 'p'").unwrap();
assert_eq!(index_name(&stmt), Some("idx"));
}
#[test]
fn insert_rejects_a_qualifier_and_reports_the_table_name_position() {
let err = parse("INSERT INTO \"abcdefgh\".\"idx\" VALUE {'pk':'a'}").unwrap_err();
assert_eq!(
err,
ParseError::Validation(
"FROM clause may only contain a single table name in data \
manipulation statements at 1:13:10"
.to_string()
)
);
let err = parse("INSERT INTO \"abcdefgh\".\"idx\" VALUE {'pk':'a'}").unwrap_err();
assert!(err.to_string().ends_with("at 1:17:10"), "got {err}");
let err = parse("INSERT INTO\n\"abcdefgh\".\"idx\" VALUE {'pk':'a'}").unwrap_err();
assert!(err.to_string().ends_with("at 2:1:10"), "got {err}");
let err = parse("INSERT INTO abcdefgh.idx VALUE {'pk':'a'}").unwrap_err();
assert!(err.to_string().ends_with("at 1:13:8"), "got {err}");
}
#[test]
fn insert_without_a_qualifier_is_unaffected() {
let stmt = parse("INSERT INTO \"T\" VALUE {'pk':'a'}").unwrap();
assert_eq!(table_name(&stmt), Some("T"));
assert_eq!(index_name(&stmt), None);
}
#[test]
fn an_unterminated_quote_is_rejected_rather_than_panicking() {
for stmt in [
"SELECT * FROM \"",
"SELECT * FROM \"T\".\"",
"SELECT * FROM \"unterminated",
"SELECT * FROM \"T\" WHERE pk = 'oops",
] {
let err = parse(stmt).unwrap_err();
assert!(
matches!(err, ParseError::Syntax(_)),
"{stmt:?} should be a syntax error, got {err:?}"
);
}
}
#[test]
fn unquote_is_total() {
for input in ["", "\"", "'", "\"\"", "''", "\"a\"", "a"] {
let _ = unquote(input);
}
}
#[test]
fn a_trailing_dot_with_no_index_name_is_an_error() {
assert!(parse("SELECT * FROM \"T\".").is_err());
}
#[test]
fn an_empty_index_component_is_rejected() {
let err = parse("SELECT * FROM \"T\".\"\"").unwrap_err();
assert_eq!(
err,
ParseError::Validation("Path component cannot be an empty string".to_string())
);
}
#[test]
fn a_three_part_name_is_rejected() {
let err = parse("SELECT * FROM \"T\".\"idx\".\"more\"").unwrap_err();
assert_eq!(
err,
ParseError::Validation(
"A path may contain at most 2 components in the FROM clause".to_string()
)
);
}
}