Skip to main content

dynoxide/partiql/
parser.rs

1//! PartiQL statement parser.
2//!
3//! Parses a subset of PartiQL relevant to DynamoDB:
4//! - `SELECT [projections] FROM "table" [WHERE conditions]`
5//! - INSERT INTO "table" VALUE { ... } [IF NOT EXISTS]
6//! - UPDATE "table" SET path = value [REMOVE attr1, attr2] [WHERE conditions]
7//! - DELETE FROM "table" [WHERE conditions]
8
9use crate::types::AttributeValue;
10use std::collections::HashMap;
11
12/// A parsed PartiQL statement.
13///
14/// The `Update` and `Delete` variants are `#[non_exhaustive]`: they carry a
15/// growing set of clauses (a `RETURNING` variant was added in 0.12.0), so
16/// downstream code must match them with `..` and cannot build them by struct
17/// literal. This keeps later clause additions non-breaking, matching the
18/// precedent set by `DynoxideError`.
19#[derive(Debug, Clone)]
20pub enum Statement {
21    Select {
22        table_name: String,
23        projections: Vec<String>, // empty = SELECT *
24        where_clause: Option<WhereClause>,
25    },
26    Insert {
27        table_name: String,
28        item: HashMap<String, PartiqlValue>,
29        if_not_exists: bool,
30    },
31    #[non_exhaustive]
32    Update {
33        table_name: String,
34        set_clauses: Vec<SetClause>,
35        remove_paths: Vec<String>,
36        where_clause: Option<WhereClause>,
37        /// The `RETURNING` variant when the statement ends with a `RETURNING`
38        /// clause. All four variants are valid on `UPDATE`. `None` when absent.
39        returning: Option<ReturningVariant>,
40    },
41    #[non_exhaustive]
42    Delete {
43        table_name: String,
44        where_clause: Option<WhereClause>,
45        /// The `RETURNING` variant when the statement ends with a `RETURNING`
46        /// clause. DynamoDB allows only `ALL OLD *` on `DELETE`; the executor
47        /// rejects the other variants. `None` when there is no clause.
48        returning: Option<ReturningVariant>,
49    },
50}
51
52/// A PartiQL `RETURNING` clause variant: the cross product of `ALL`/`MODIFIED`
53/// and `OLD`/`NEW`, as in `RETURNING MODIFIED NEW *`.
54///
55/// All four are valid on `UPDATE`; only `AllOld` is valid on `DELETE` (the
56/// executor rejects the others). The parser recognises any well-formed variant
57/// and defers semantic validity to the executor, so the rejection can carry
58/// DynamoDB's exact `ValidationException` message.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum ReturningVariant {
61    AllOld,
62    ModifiedOld,
63    AllNew,
64    ModifiedNew,
65}
66
67impl ReturningVariant {
68    /// The clause keywords as they appear after `RETURNING` (e.g. `ALL OLD`),
69    /// used to reconstruct DynamoDB's error message for a rejected variant.
70    pub fn as_sql(self) -> &'static str {
71        match self {
72            ReturningVariant::AllOld => "ALL OLD",
73            ReturningVariant::ModifiedOld => "MODIFIED OLD",
74            ReturningVariant::AllNew => "ALL NEW",
75            ReturningVariant::ModifiedNew => "MODIFIED NEW",
76        }
77    }
78}
79
80/// Extract the table name from a parsed statement.
81pub fn table_name(stmt: &Statement) -> Option<&str> {
82    match stmt {
83        Statement::Select { table_name, .. }
84        | Statement::Insert { table_name, .. }
85        | Statement::Update { table_name, .. }
86        | Statement::Delete { table_name, .. } => Some(table_name),
87    }
88}
89
90/// The `RETURNING` variant a statement carries, if any. Only `UPDATE` and
91/// `DELETE` can carry one; every other statement returns `None`.
92pub fn returning_variant(stmt: &Statement) -> Option<ReturningVariant> {
93    match stmt {
94        Statement::Update { returning, .. } | Statement::Delete { returning, .. } => *returning,
95        _ => None,
96    }
97}
98
99/// A SET clause in an UPDATE statement.
100#[derive(Debug, Clone)]
101pub struct SetClause {
102    pub path: String,
103    pub value: SetValue,
104}
105
106/// A value on the right-hand side of a SET assignment.
107/// Supports simple values and binary arithmetic expressions.
108#[derive(Debug, Clone, PartialEq)]
109pub enum SetValue {
110    /// A simple value (literal or parameter).
111    Simple(PartiqlValue),
112    /// `path + value` — add the value to the attribute at path.
113    Add(String, PartiqlValue),
114    /// `path - value` — subtract the value from the attribute at path.
115    Sub(String, PartiqlValue),
116    /// `list_append(path, value)` or `list_append(value, path)`.
117    ListAppend(PartiqlValue, PartiqlValue),
118}
119
120/// A WHERE clause with OR-group semantics.
121///
122/// Groups are OR-joined; conditions within each group are AND-joined.
123/// `WHERE a = 1 AND b = 2 OR c = 3` parses as `[[a=1, b=2], [c=3]]`.
124#[derive(Debug, Clone)]
125pub struct WhereClause {
126    /// OR-groups: outer = OR, inner = AND.
127    pub groups: Vec<Vec<WhereCondition>>,
128}
129
130impl WhereClause {
131    /// Create a WhereClause from a single group of AND-joined conditions.
132    pub fn from_conditions(conditions: Vec<WhereCondition>) -> Self {
133        Self {
134            groups: vec![conditions],
135        }
136    }
137
138    /// Create a WhereClause from multiple OR-groups.
139    pub fn from_groups(groups: Vec<Vec<WhereCondition>>) -> Self {
140        Self { groups }
141    }
142}
143
144/// A single condition in a WHERE clause — either a comparison or a function call.
145#[derive(Debug, Clone)]
146pub enum WhereCondition {
147    Comparison(Condition),
148    Exists(String),
149    NotExists(String),
150    BeginsWith(String, PartiqlValue),
151    NotBeginsWith(String, PartiqlValue),
152    Between(String, PartiqlValue, PartiqlValue),
153    In(String, Vec<PartiqlValue>),
154    Contains(String, PartiqlValue),
155    IsMissing(String),
156    IsNotMissing(String),
157}
158
159/// A comparison condition (path op value).
160#[derive(Debug, Clone)]
161pub struct Condition {
162    pub path: String,
163    pub op: CompOp,
164    pub value: PartiqlValue,
165}
166
167/// Comparison operator.
168#[derive(Debug, Clone, PartialEq)]
169pub enum CompOp {
170    Eq,
171    Ne,
172    Lt,
173    Le,
174    Gt,
175    Ge,
176}
177
178/// A value in a PartiQL expression — either a literal or a parameter placeholder.
179#[derive(Debug, Clone, PartialEq)]
180pub enum PartiqlValue {
181    Literal(AttributeValue),
182    Parameter(usize), // 0-based index into the Parameters array
183}
184
185/// Parse a PartiQL statement string.
186pub fn parse(input: &str) -> Result<Statement, String> {
187    let mut tokenizer = Tokenizer::new(input)?;
188    let first = tokenizer
189        .next_token()?
190        .ok_or("Empty statement")?
191        .to_uppercase();
192
193    match first.as_str() {
194        "SELECT" => parse_select(&mut tokenizer),
195        "INSERT" => parse_insert(&mut tokenizer),
196        "UPDATE" => parse_update(&mut tokenizer),
197        "DELETE" => parse_delete(&mut tokenizer),
198        // A statement that does not begin with a DML keyword is not valid
199        // PartiQL; DynamoDB reports this as "Expected data manipulation".
200        _ => Err("Expected data manipulation".to_string()),
201    }
202}
203
204fn parse_select(t: &mut Tokenizer) -> Result<Statement, String> {
205    // Parse projections
206    let projections = parse_projections(t)?;
207
208    // Expect FROM
209    expect_keyword(t, "FROM")?;
210
211    // Parse table name
212    let table_name = parse_table_name(t)?;
213
214    // Optional WHERE clause
215    let where_clause = parse_optional_where(t)?;
216
217    Ok(Statement::Select {
218        table_name,
219        projections,
220        where_clause,
221    })
222}
223
224fn parse_projections(t: &mut Tokenizer) -> Result<Vec<String>, String> {
225    let tok = t.peek_token()?.ok_or("Expected projection")?;
226
227    if tok == "*" {
228        t.next_token()?; // consume *
229        return Ok(Vec::new());
230    }
231
232    // Check for COUNT(*)
233    if tok.eq_ignore_ascii_case("COUNT") {
234        t.next_token()?; // consume COUNT
235        expect_char(t, "(")?;
236        expect_char(t, "*")?;
237        expect_char(t, ")")?;
238        return Ok(vec!["COUNT(*)".to_string()]);
239    }
240
241    let mut projections = Vec::new();
242    loop {
243        let name = t
244            .next_token()?
245            .ok_or("Expected projection attribute name")?;
246        let mut path = unquote(&name);
247
248        // Greedily consume dot-separated segments and array indexes
249        loop {
250            match t.peek_token()? {
251                Some(ref s) if s == "." => {
252                    t.next_token()?; // consume dot
253                    let segment = t.next_token()?.ok_or("Expected attribute name after '.'")?;
254                    path.push('.');
255                    path.push_str(&unquote(&segment));
256                }
257                Some(ref s) if s == "[" => {
258                    t.next_token()?; // consume [
259                    let idx = t.next_token()?.ok_or("Expected index in '[]'")?;
260                    let close = t.next_token()?.ok_or("Expected ']'")?;
261                    if close != "]" {
262                        return Err(format!("Expected ']' but got '{close}'"));
263                    }
264                    path.push('[');
265                    path.push_str(&idx);
266                    path.push(']');
267                }
268                _ => break,
269            }
270        }
271
272        projections.push(path);
273
274        match t.peek_token()? {
275            Some(ref s) if s == "," => {
276                t.next_token()?; // consume comma
277            }
278            _ => break,
279        }
280    }
281
282    Ok(projections)
283}
284
285fn parse_insert(t: &mut Tokenizer) -> Result<Statement, String> {
286    expect_keyword(t, "INTO")?;
287    let table_name = parse_table_name(t)?;
288    expect_keyword(t, "VALUE")?;
289
290    // Parse the item literal as a map of possibly-parameterised values
291    let item = parse_item_literal_partiql(t)?;
292
293    // Check for IF NOT EXISTS
294    let if_not_exists = if let Some(ref tok) = t.peek_token()? {
295        if tok.eq_ignore_ascii_case("IF") {
296            t.next_token()?; // consume IF
297            expect_keyword(t, "NOT")?;
298            expect_keyword(t, "EXISTS")?;
299            true
300        } else {
301            false
302        }
303    } else {
304        false
305    };
306
307    Ok(Statement::Insert {
308        table_name,
309        item,
310        if_not_exists,
311    })
312}
313
314fn parse_update(t: &mut Tokenizer) -> Result<Statement, String> {
315    let table_name = parse_table_name(t)?;
316
317    // SET and REMOVE are both optional but at least one must be present.
318    // Parse SET clauses if the next keyword is SET.
319    let mut set_clauses = Vec::new();
320    let mut remove_paths = Vec::new();
321
322    if let Some(ref tok) = t.peek_token()? {
323        if tok.eq_ignore_ascii_case("SET") {
324            t.next_token()?; // consume SET
325            loop {
326                let path_tok = t.next_token()?.ok_or("Expected attribute path in SET")?;
327                let path = parse_dotted_path_from_token(&path_tok, t)?;
328
329                let eq = t.next_token()?.ok_or("Expected '='")?;
330                if eq != "=" {
331                    return Err(format!("Expected '=' but got '{eq}'"));
332                }
333
334                let value = parse_set_value(t)?;
335                set_clauses.push(SetClause { path, value });
336
337                match t.peek_token()? {
338                    Some(ref s) if s == "," => {
339                        t.next_token()?; // consume comma
340                    }
341                    _ => break,
342                }
343            }
344        }
345    }
346
347    // Check for REMOVE keyword
348    if let Some(ref tok) = t.peek_token()? {
349        if tok.eq_ignore_ascii_case("REMOVE") {
350            t.next_token()?; // consume REMOVE
351            loop {
352                let path_tok = t.next_token()?.ok_or("Expected attribute path in REMOVE")?;
353                let path = parse_dotted_path_from_token(&path_tok, t)?;
354                remove_paths.push(path);
355                match t.peek_token()? {
356                    Some(ref s) if s == "," => {
357                        t.next_token()?;
358                    }
359                    _ => break,
360                }
361            }
362        }
363    }
364
365    if set_clauses.is_empty() && remove_paths.is_empty() {
366        return Err("UPDATE requires at least one SET or REMOVE clause".to_string());
367    }
368
369    let where_clause = parse_optional_where(t)?;
370    let returning = parse_optional_returning(t)?;
371
372    Ok(Statement::Update {
373        table_name,
374        set_clauses,
375        remove_paths,
376        where_clause,
377        returning,
378    })
379}
380
381/// Parse the right-hand side of a SET assignment: `value`, `path + value`, `path - value`,
382/// or `list_append(a, b)`.
383fn parse_set_value(t: &mut Tokenizer) -> Result<SetValue, String> {
384    // Check for list_append function
385    if let Some(ref tok) = t.peek_token()? {
386        if tok.eq_ignore_ascii_case("list_append") {
387            t.next_token()?; // consume list_append
388            expect_char(t, "(")?;
389            let first = parse_value(t)?;
390            let comma = t.next_token()?.ok_or("Expected ',' in list_append")?;
391            if comma != "," {
392                return Err(format!("Expected ',' but got '{comma}'"));
393            }
394            let second = parse_value(t)?;
395            expect_char(t, ")")?;
396            return Ok(SetValue::ListAppend(first, second));
397        }
398    }
399
400    let first = parse_value(t)?;
401
402    // Peek for + or -
403    match t.peek_token()? {
404        Some(ref s) if s == "+" => {
405            t.next_token()?; // consume +
406            let second = parse_value(t)?;
407            // The first value should be a path reference (attribute name).
408            // In PartiQL, `SET x = x + 1` means add 1 to the current value of x.
409            let attr_path = match &first {
410                PartiqlValue::Literal(AttributeValue::S(s)) => s.clone(),
411                // If first is an unquoted identifier that was mistakenly parsed as something
412                // else, we need to handle it. But identifiers in SET RHS would have been
413                // consumed as unknown tokens and errored. We'll handle the common case
414                // where parse_value can't parse an identifier — see below.
415                _ => {
416                    return Err(
417                        "Expected attribute path on left side of '+' expression".to_string()
418                    );
419                }
420            };
421            Ok(SetValue::Add(attr_path, second))
422        }
423        Some(ref s) if s == "-" => {
424            t.next_token()?; // consume -
425            let second = parse_value(t)?;
426            let attr_path = match &first {
427                PartiqlValue::Literal(AttributeValue::S(s)) => s.clone(),
428                _ => {
429                    return Err(
430                        "Expected attribute path on left side of '-' expression".to_string()
431                    );
432                }
433            };
434            Ok(SetValue::Sub(attr_path, second))
435        }
436        _ => Ok(SetValue::Simple(first)),
437    }
438}
439
440fn parse_delete(t: &mut Tokenizer) -> Result<Statement, String> {
441    expect_keyword(t, "FROM")?;
442    let table_name = parse_table_name(t)?;
443    let where_clause = parse_optional_where(t)?;
444    let returning = parse_optional_returning(t)?;
445
446    Ok(Statement::Delete {
447        table_name,
448        where_clause,
449        returning,
450    })
451}
452
453/// Parse an optional trailing `RETURNING <ALL|MODIFIED> <OLD|NEW> *` clause.
454///
455/// Recognises any of the four well-formed variants and returns it; a malformed
456/// clause (missing `*`, unknown keywords, trailing tokens) is a syntax error.
457/// Which variants a given command actually permits is enforced by the executor,
458/// so it can surface DynamoDB's exact validation message.
459fn parse_optional_returning(t: &mut Tokenizer) -> Result<Option<ReturningVariant>, String> {
460    match t.peek_token()? {
461        Some(ref s) if s.eq_ignore_ascii_case("RETURNING") => {
462            t.next_token()?; // consume RETURNING
463            let first = t
464                .next_token()?
465                .ok_or("Expected returning variant after RETURNING")?;
466            let second = t
467                .next_token()?
468                .ok_or("Expected returning variant after RETURNING")?;
469            let star = t.next_token()?.ok_or("Expected '*' in RETURNING clause")?;
470            if star != "*" || t.peek_token()?.is_some() {
471                return Err(
472                    "Malformed RETURNING clause; expected RETURNING <ALL|MODIFIED> <OLD|NEW> *"
473                        .to_string(),
474                );
475            }
476            let variant = match (
477                first.to_ascii_uppercase().as_str(),
478                second.to_ascii_uppercase().as_str(),
479            ) {
480                ("ALL", "OLD") => ReturningVariant::AllOld,
481                ("MODIFIED", "OLD") => ReturningVariant::ModifiedOld,
482                ("ALL", "NEW") => ReturningVariant::AllNew,
483                ("MODIFIED", "NEW") => ReturningVariant::ModifiedNew,
484                _ => {
485                    return Err(format!(
486                        "Unsupported RETURNING clause: RETURNING {} {} *",
487                        first.to_ascii_uppercase(),
488                        second.to_ascii_uppercase()
489                    ));
490                }
491            };
492            Ok(Some(variant))
493        }
494        _ => Ok(None),
495    }
496}
497
498fn parse_table_name(t: &mut Tokenizer) -> Result<String, String> {
499    let name = t.next_token()?.ok_or("Expected table name")?;
500    Ok(unquote(&name))
501}
502
503fn parse_optional_where(t: &mut Tokenizer) -> Result<Option<WhereClause>, String> {
504    match t.peek_token()? {
505        Some(ref s) if s.eq_ignore_ascii_case("WHERE") => {
506            t.next_token()?; // consume WHERE
507            let groups = parse_conditions_with_or(t)?;
508            Ok(Some(WhereClause::from_groups(groups)))
509        }
510        _ => Ok(None),
511    }
512}
513
514/// Parse conditions supporting both AND and OR.
515/// Returns a list of OR-groups, where each group is a list of AND-joined conditions.
516fn parse_conditions_with_or(t: &mut Tokenizer) -> Result<Vec<Vec<WhereCondition>>, String> {
517    let mut groups: Vec<Vec<WhereCondition>> = Vec::new();
518    let mut current_group: Vec<WhereCondition> = Vec::new();
519
520    loop {
521        let condition = parse_single_condition(t)?;
522        current_group.push(condition);
523
524        match t.peek_token()? {
525            Some(ref s) if s.eq_ignore_ascii_case("AND") => {
526                t.next_token()?; // consume AND — continue in current group
527            }
528            Some(ref s) if s.eq_ignore_ascii_case("OR") => {
529                t.next_token()?; // consume OR — start new group
530                groups.push(current_group);
531                current_group = Vec::new();
532            }
533            _ => break,
534        }
535    }
536
537    groups.push(current_group);
538    Ok(groups)
539}
540
541/// Parse a single condition (comparison, function call, etc.).
542fn parse_single_condition(t: &mut Tokenizer) -> Result<WhereCondition, String> {
543    let tok = t.next_token()?.ok_or("Expected condition in WHERE")?;
544    let tok_upper = tok.to_uppercase();
545
546    match tok_upper.as_str() {
547        "EXISTS" => {
548            expect_char(t, "(")?;
549            let path = parse_function_path(t)?;
550            expect_char(t, ")")?;
551            Ok(WhereCondition::Exists(path))
552        }
553        "BEGINS_WITH" => {
554            expect_char(t, "(")?;
555            let path = parse_function_path(t)?;
556            let comma = t.next_token()?.ok_or("Expected ',' in BEGINS_WITH")?;
557            if comma != "," {
558                return Err(format!("Expected ',' but got '{comma}'"));
559            }
560            let value = parse_value(t)?;
561            expect_char(t, ")")?;
562            Ok(WhereCondition::BeginsWith(path, value))
563        }
564        "CONTAINS" => {
565            expect_char(t, "(")?;
566            let path = parse_function_path(t)?;
567            let comma = t.next_token()?.ok_or("Expected ',' in CONTAINS")?;
568            if comma != "," {
569                return Err(format!("Expected ',' but got '{comma}'"));
570            }
571            let value = parse_value(t)?;
572            expect_char(t, ")")?;
573            Ok(WhereCondition::Contains(path, value))
574        }
575        "NOT" => {
576            let func = t.next_token()?.ok_or("Expected function name after NOT")?;
577            if func.eq_ignore_ascii_case("EXISTS") {
578                expect_char(t, "(")?;
579                let path = parse_function_path(t)?;
580                expect_char(t, ")")?;
581                Ok(WhereCondition::NotExists(path))
582            } else if func.eq_ignore_ascii_case("BEGINS_WITH") {
583                expect_char(t, "(")?;
584                let path = parse_function_path(t)?;
585                let comma = t.next_token()?.ok_or("Expected ',' in NOT BEGINS_WITH")?;
586                if comma != "," {
587                    return Err(format!("Expected ',' but got '{comma}'"));
588                }
589                let value = parse_value(t)?;
590                expect_char(t, ")")?;
591                Ok(WhereCondition::NotBeginsWith(path, value))
592            } else {
593                Err(format!("Unsupported NOT function: {func}"))
594            }
595        }
596        _ => {
597            // Regular comparison or BETWEEN / IN / IS MISSING
598            // The token might be the start of a dotted path
599            let path = parse_dotted_path_from_token(&tok, t)?;
600
601            // Peek at the next token to decide which form this is
602            let next = t
603                .peek_token()?
604                .ok_or("Expected operator after attribute path")?;
605            let next_upper = next.to_uppercase();
606
607            match next_upper.as_str() {
608                "BETWEEN" => {
609                    t.next_token()?; // consume BETWEEN
610                    let low = parse_value(t)?;
611                    expect_keyword(t, "AND")?;
612                    let high = parse_value(t)?;
613                    Ok(WhereCondition::Between(path, low, high))
614                }
615                "IN" => {
616                    t.next_token()?; // consume IN
617                    // Accept both the parenthesised form `IN (...)` (DynamoDB
618                    // FilterExpression style) and the bracket form `IN [...]`
619                    // (PartiQL style); the closing token must match the opener.
620                    let open = t.next_token()?.ok_or("Expected '(' or '[' after IN")?;
621                    let close_char = match open.as_str() {
622                        "(" => ")",
623                        "[" => "]",
624                        other => {
625                            return Err(format!("Expected '(' or '[' after IN, got '{other}'"));
626                        }
627                    };
628                    let mut values = Vec::new();
629                    loop {
630                        let peek = t.peek_token()?.ok_or("Unexpected end of IN list")?;
631                        if peek == close_char {
632                            t.next_token()?; // consume closing bracket
633                            break;
634                        }
635                        if peek == "," {
636                            t.next_token()?; // consume comma
637                            continue;
638                        }
639                        values.push(parse_value(t)?);
640                    }
641                    Ok(WhereCondition::In(path, values))
642                }
643                "IS" => {
644                    t.next_token()?; // consume IS
645                    let kw = t.next_token()?.ok_or("Expected MISSING or NOT after IS")?;
646                    let kw_upper = kw.to_uppercase();
647                    match kw_upper.as_str() {
648                        "MISSING" => Ok(WhereCondition::IsMissing(path)),
649                        "NOT" => {
650                            expect_keyword(t, "MISSING")?;
651                            Ok(WhereCondition::IsNotMissing(path))
652                        }
653                        other => Err(format!(
654                            "Expected MISSING or NOT MISSING after IS, got '{other}'"
655                        )),
656                    }
657                }
658                _ => {
659                    // Standard comparison: path op value
660                    let op_tok = t.next_token()?.ok_or("Expected comparison operator")?;
661                    let op = match op_tok.as_str() {
662                        "=" => CompOp::Eq,
663                        "<>" | "!=" => CompOp::Ne,
664                        "<" => CompOp::Lt,
665                        "<=" => CompOp::Le,
666                        ">" => CompOp::Gt,
667                        ">=" => CompOp::Ge,
668                        other => return Err(format!("Unknown operator: {other}")),
669                    };
670                    let value = parse_value(t)?;
671                    Ok(WhereCondition::Comparison(Condition { path, op, value }))
672                }
673            }
674        }
675    }
676}
677
678/// Parse a dotted path starting from an already-consumed first token.
679/// Greedily consumes `.segment` continuations.
680fn parse_dotted_path_from_token(first_tok: &str, t: &mut Tokenizer) -> Result<String, String> {
681    let mut path = unquote(first_tok);
682    while let Some(ref next) = t.peek_token()? {
683        if next == "." {
684            t.next_token()?; // consume dot
685            let seg = t.next_token()?.ok_or("Expected attribute name after '.'")?;
686            path.push('.');
687            path.push_str(&unquote(&seg));
688        } else if next == "[" {
689            t.next_token()?; // consume [
690            let idx = t.next_token()?.ok_or("Expected index in '[]'")?;
691            let close = t.next_token()?.ok_or("Expected ']'")?;
692            if close != "]" {
693                return Err(format!("Expected ']' but got '{close}'"));
694            }
695            path.push('[');
696            path.push_str(&idx);
697            path.push(']');
698        } else {
699            break;
700        }
701    }
702    Ok(path)
703}
704
705/// Parse a path inside a function call (e.g. EXISTS, BEGINS_WITH, CONTAINS).
706/// Supports dotted paths like `address.city`.
707fn parse_function_path(t: &mut Tokenizer) -> Result<String, String> {
708    let tok = t.next_token()?.ok_or("Expected path in function")?;
709    parse_dotted_path_from_token(&tok, t)
710}
711
712fn expect_char(t: &mut Tokenizer, expected: &str) -> Result<(), String> {
713    let tok = t.next_token()?.ok_or(format!("Expected '{expected}'"))?;
714    if tok != expected {
715        return Err(format!("Expected '{expected}' but got '{tok}'"));
716    }
717    Ok(())
718}
719
720fn parse_value(t: &mut Tokenizer) -> Result<PartiqlValue, String> {
721    let tok = t.next_token()?.ok_or("Expected value")?;
722
723    if tok == "?" {
724        let idx = t.next_param_index();
725        return Ok(PartiqlValue::Parameter(idx));
726    }
727
728    // String literal: 'value'
729    if tok.starts_with('\'') && tok.ends_with('\'') && tok.len() >= 2 {
730        let s = tok[1..tok.len() - 1].to_string();
731        return Ok(PartiqlValue::Literal(AttributeValue::S(s)));
732    }
733
734    // Set literal: << val1, val2 >>
735    if tok == "<" {
736        if let Some(ref next) = t.peek_token()? {
737            if next == "<" {
738                t.next_token()?; // consume second <
739                let mut elements = Vec::new();
740                loop {
741                    let peek = t.peek_token()?.ok_or("Unexpected end of set literal")?;
742                    if peek == ">" {
743                        t.next_token()?; // consume first >
744                        // Consume second >
745                        let next_close = t.peek_token()?;
746                        if next_close.as_deref() == Some(">") {
747                            t.next_token()?;
748                        }
749                        break;
750                    }
751                    if peek == "," {
752                        t.next_token()?;
753                        continue;
754                    }
755                    elements.push(parse_value(t)?);
756                }
757                return set_literal_to_value(elements);
758            }
759        }
760    }
761
762    // List literal: [val1, val2]
763    if tok == "[" {
764        let mut items = Vec::new();
765        loop {
766            let peek = t.peek_token()?.ok_or("Unexpected end of list")?;
767            if peek == "]" {
768                t.next_token()?;
769                break;
770            }
771            if peek == "," {
772                t.next_token()?;
773                continue;
774            }
775            items.push(parse_value(t)?);
776        }
777        // We can only produce a Literal list if all elements are literals
778        let mut avs = Vec::new();
779        for item in items {
780            match item {
781                PartiqlValue::Literal(av) => avs.push(av),
782                PartiqlValue::Parameter(_) => {
783                    // Can't build a static list with parameters in it at parse time.
784                    // For now, return an error — a more complete solution would
785                    // defer resolution.
786                    return Err(
787                        "Parameter placeholders inside list literals are not yet supported"
788                            .to_string(),
789                    );
790                }
791            }
792        }
793        return Ok(PartiqlValue::Literal(AttributeValue::L(avs)));
794    }
795
796    // Map literal: { 'key': value, ... }
797    if tok == "{" {
798        let mut map = HashMap::new();
799        loop {
800            let peek = t.peek_token()?.ok_or("Unexpected end of map literal")?;
801            if peek == "}" {
802                t.next_token()?;
803                break;
804            }
805            if peek == "," {
806                t.next_token()?;
807                continue;
808            }
809            let key_tok = t.next_token()?.ok_or("Expected key in map literal")?;
810            let key = unquote(&key_tok);
811            let colon = t.next_token()?.ok_or("Expected ':'")?;
812            if colon != ":" {
813                return Err(format!("Expected ':' but got '{colon}'"));
814            }
815            let val = parse_value(t)?;
816            match val {
817                PartiqlValue::Literal(av) => {
818                    map.insert(key, av);
819                }
820                PartiqlValue::Parameter(_) => {
821                    return Err(
822                        "Parameter placeholders inside map literals are not yet supported"
823                            .to_string(),
824                    );
825                }
826            }
827        }
828        return Ok(PartiqlValue::Literal(AttributeValue::M(map)));
829    }
830
831    // Negative number: `-` followed by a numeric token
832    if tok == "-" || tok == "+" {
833        if let Some(ref next) = t.peek_token()? {
834            if next.starts_with(|c: char| c.is_ascii_digit()) {
835                let num = t.next_token()?.unwrap();
836                return Ok(PartiqlValue::Literal(AttributeValue::N(format!(
837                    "{tok}{num}"
838                ))));
839            }
840        }
841    }
842
843    // Numeric literal
844    if tok.starts_with(|c: char| c.is_ascii_digit()) {
845        return Ok(PartiqlValue::Literal(AttributeValue::N(tok)));
846    }
847
848    // Boolean / null
849    match tok.to_uppercase().as_str() {
850        "TRUE" => return Ok(PartiqlValue::Literal(AttributeValue::BOOL(true))),
851        "FALSE" => return Ok(PartiqlValue::Literal(AttributeValue::BOOL(false))),
852        "NULL" => return Ok(PartiqlValue::Literal(AttributeValue::NULL(true))),
853        _ => {}
854    }
855
856    // Bare identifier — treat as a string (attribute name reference in SET expressions)
857    // This handles cases like `SET x = x + 1` where `x` on the RHS is an identifier.
858    if tok
859        .chars()
860        .next()
861        .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
862    {
863        // Consume any dotted path continuation
864        let mut path = tok.clone();
865        while let Some(ref next) = t.peek_token()? {
866            if next == "." {
867                t.next_token()?;
868                let seg = t.next_token()?.ok_or("Expected attribute name after '.'")?;
869                path.push('.');
870                path.push_str(&unquote(&seg));
871            } else {
872                break;
873            }
874        }
875        return Ok(PartiqlValue::Literal(AttributeValue::S(path)));
876    }
877
878    Err(format!("Unexpected value token: {tok}"))
879}
880
881/// Convert parsed set literal elements into a DynamoDB set type (SS, NS, or BS).
882fn set_literal_to_value(elements: Vec<PartiqlValue>) -> Result<PartiqlValue, String> {
883    if elements.is_empty() {
884        return Err("Set literals cannot be empty".to_string());
885    }
886
887    // Determine type from first element
888    let first = match &elements[0] {
889        PartiqlValue::Literal(av) => av,
890        PartiqlValue::Parameter(_) => {
891            return Err("Parameter placeholders in set literals are not supported".to_string());
892        }
893    };
894
895    match first {
896        AttributeValue::S(_) => {
897            let mut ss = Vec::new();
898            for elem in &elements {
899                match elem {
900                    PartiqlValue::Literal(AttributeValue::S(s)) => ss.push(s.clone()),
901                    _ => return Err("Mixed types in string set literal".to_string()),
902                }
903            }
904            Ok(PartiqlValue::Literal(AttributeValue::SS(ss)))
905        }
906        AttributeValue::N(_) => {
907            let mut ns = Vec::new();
908            for elem in &elements {
909                match elem {
910                    PartiqlValue::Literal(AttributeValue::N(n)) => ns.push(n.clone()),
911                    _ => return Err("Mixed types in number set literal".to_string()),
912                }
913            }
914            Ok(PartiqlValue::Literal(AttributeValue::NS(ns)))
915        }
916        _ => Err(format!(
917            "Unsupported element type in set literal: {first:?}"
918        )),
919    }
920}
921
922/// Parse a `{ 'key': 'value', ... }` item literal into a DynamoDB attribute map.
923fn parse_item_literal(t: &mut Tokenizer) -> Result<HashMap<String, AttributeValue>, String> {
924    let open = t.next_token()?.ok_or("Expected '{'")?;
925    if open != "{" {
926        return Err(format!("Expected '{{' but got '{open}'"));
927    }
928
929    let mut item = HashMap::new();
930
931    loop {
932        let tok = t.peek_token()?.ok_or("Unexpected end of item literal")?;
933        if tok == "}" {
934            t.next_token()?; // consume }
935            break;
936        }
937
938        // Skip commas between entries
939        if tok == "," {
940            t.next_token()?;
941            continue;
942        }
943
944        // Parse key
945        let key_tok = t.next_token()?.ok_or("Expected key in item literal")?;
946        let key = unquote(&key_tok);
947
948        let colon = t.next_token()?.ok_or("Expected ':'")?;
949        if colon != ":" {
950            return Err(format!("Expected ':' but got '{colon}'"));
951        }
952
953        // Parse value
954        let val = parse_item_value(t)?;
955        item.insert(key, val);
956    }
957
958    Ok(item)
959}
960
961/// Parse a value inside an item literal (supports nested maps, lists, set literals, etc.).
962fn parse_item_value(t: &mut Tokenizer) -> Result<AttributeValue, String> {
963    let tok = t.peek_token()?.ok_or("Expected value")?;
964
965    if tok == "{" {
966        // Nested map
967        let inner = parse_item_literal(t)?;
968        return Ok(AttributeValue::M(inner));
969    }
970
971    if tok == "[" {
972        // List
973        t.next_token()?; // consume [
974        let mut items = Vec::new();
975        loop {
976            let peek = t.peek_token()?.ok_or("Unexpected end of list")?;
977            if peek == "]" {
978                t.next_token()?;
979                break;
980            }
981            if peek == "," {
982                t.next_token()?;
983                continue;
984            }
985            items.push(parse_item_value(t)?);
986        }
987        return Ok(AttributeValue::L(items));
988    }
989
990    // Set literal: << val1, val2 >>
991    if tok == "<" {
992        if let Some(ref next_tok) = t.peek_token_at(1)? {
993            if next_tok == "<" {
994                t.next_token()?; // consume first <
995                t.next_token()?; // consume second <
996                let mut elements = Vec::new();
997                loop {
998                    let peek = t.peek_token()?.ok_or("Unexpected end of set literal")?;
999                    if peek == ">" {
1000                        t.next_token()?; // consume first >
1001                        if t.peek_token()?.as_deref() == Some(">") {
1002                            t.next_token()?; // consume second >
1003                        }
1004                        break;
1005                    }
1006                    if peek == "," {
1007                        t.next_token()?;
1008                        continue;
1009                    }
1010                    elements.push(parse_item_value(t)?);
1011                }
1012                return item_value_set_literal(elements);
1013            }
1014        }
1015    }
1016
1017    // Scalar value
1018    let tok = t.next_token()?.ok_or("Expected value")?;
1019
1020    // String
1021    if tok.starts_with('\'') && tok.ends_with('\'') && tok.len() >= 2 {
1022        return Ok(AttributeValue::S(tok[1..tok.len() - 1].to_string()));
1023    }
1024
1025    // Negative number: `-` followed by a numeric token
1026    if tok == "-" || tok == "+" {
1027        if let Some(ref next) = t.peek_token()? {
1028            if next.starts_with(|c: char| c.is_ascii_digit()) {
1029                let num = t.next_token()?.unwrap();
1030                return Ok(AttributeValue::N(format!("{tok}{num}")));
1031            }
1032        }
1033    }
1034
1035    // Number
1036    if tok.starts_with(|c: char| c.is_ascii_digit()) {
1037        return Ok(AttributeValue::N(tok));
1038    }
1039
1040    match tok.to_uppercase().as_str() {
1041        "TRUE" => Ok(AttributeValue::BOOL(true)),
1042        "FALSE" => Ok(AttributeValue::BOOL(false)),
1043        "NULL" => Ok(AttributeValue::NULL(true)),
1044        _ => Err(format!("Unexpected value in item literal: {tok}")),
1045    }
1046}
1047
1048/// Convert a list of item-literal values into a DynamoDB set type.
1049fn item_value_set_literal(elements: Vec<AttributeValue>) -> Result<AttributeValue, String> {
1050    if elements.is_empty() {
1051        return Err("Set literals cannot be empty".to_string());
1052    }
1053    match &elements[0] {
1054        AttributeValue::S(_) => {
1055            let mut ss = Vec::new();
1056            for e in elements {
1057                match e {
1058                    AttributeValue::S(s) => ss.push(s),
1059                    _ => return Err("Mixed types in string set literal".to_string()),
1060                }
1061            }
1062            Ok(AttributeValue::SS(ss))
1063        }
1064        AttributeValue::N(_) => {
1065            let mut ns = Vec::new();
1066            for e in elements {
1067                match e {
1068                    AttributeValue::N(n) => ns.push(n),
1069                    _ => return Err("Mixed types in number set literal".to_string()),
1070                }
1071            }
1072            Ok(AttributeValue::NS(ns))
1073        }
1074        _ => Err(format!(
1075            "Unsupported element type in set literal: {:?}",
1076            elements[0]
1077        )),
1078    }
1079}
1080
1081/// Parse a `{ 'key': value, ... }` item literal where values may be `?` parameter placeholders.
1082/// Returns `PartiqlValue` wrappers so parameters can be resolved at execution time.
1083fn parse_item_literal_partiql(t: &mut Tokenizer) -> Result<HashMap<String, PartiqlValue>, String> {
1084    let open = t.next_token()?.ok_or("Expected '{'")?;
1085    if open != "{" {
1086        return Err(format!("Expected '{{' but got '{open}'"));
1087    }
1088
1089    let mut item = HashMap::new();
1090
1091    loop {
1092        let tok = t.peek_token()?.ok_or("Unexpected end of item literal")?;
1093        if tok == "}" {
1094            t.next_token()?; // consume }
1095            break;
1096        }
1097
1098        // Skip commas between entries
1099        if tok == "," {
1100            t.next_token()?;
1101            continue;
1102        }
1103
1104        // Parse key
1105        let key_tok = t.next_token()?.ok_or("Expected key in item literal")?;
1106        let key = unquote(&key_tok);
1107
1108        let colon = t.next_token()?.ok_or("Expected ':'")?;
1109        if colon != ":" {
1110            return Err(format!("Expected ':' but got '{colon}'"));
1111        }
1112
1113        // Parse value (may be a parameter placeholder)
1114        let val = parse_item_value_partiql(t)?;
1115        item.insert(key, val);
1116    }
1117
1118    Ok(item)
1119}
1120
1121/// Parse a value inside an item literal, supporting `?` parameter placeholders
1122/// and nested maps/lists (which are stored as `PartiqlValue::Literal`).
1123fn parse_item_value_partiql(t: &mut Tokenizer) -> Result<PartiqlValue, String> {
1124    let tok = t.peek_token()?.ok_or("Expected value")?;
1125
1126    if tok == "?" {
1127        t.next_token()?; // consume ?
1128        let idx = t.next_param_index();
1129        return Ok(PartiqlValue::Parameter(idx));
1130    }
1131
1132    // For lists, use parse_value which supports `?` inside list elements
1133    if tok == "[" {
1134        return parse_value(t);
1135    }
1136
1137    // For nested maps, use recursive partiql parsing to support `?`
1138    if tok == "{" {
1139        // Parse nested map with partiql-aware parser
1140        let inner = parse_item_literal_partiql(t)?;
1141        // Check if all values are literals — if so, collapse to a single Literal
1142        let mut map = HashMap::new();
1143        for (k, v) in inner {
1144            match v {
1145                PartiqlValue::Literal(av) => {
1146                    map.insert(k, av);
1147                }
1148                PartiqlValue::Parameter(_) => {
1149                    // Can't represent a map with parameter values as a single Literal.
1150                    // For now, return an error.
1151                    return Err(
1152                        "Parameter placeholders inside nested map literals are not yet fully supported"
1153                            .to_string(),
1154                    );
1155                }
1156            }
1157        }
1158        return Ok(PartiqlValue::Literal(AttributeValue::M(map)));
1159    }
1160
1161    // For set literals << >>, delegate to parse_item_value which handles them
1162    // For other scalar values, use parse_item_value and wrap
1163    let av = parse_item_value(t)?;
1164    Ok(PartiqlValue::Literal(av))
1165}
1166
1167/// Remove surrounding single or double quotes from a string.
1168fn unquote(s: &str) -> String {
1169    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
1170        s[1..s.len() - 1].to_string()
1171    } else {
1172        s.to_string()
1173    }
1174}
1175
1176fn expect_keyword(t: &mut Tokenizer, kw: &str) -> Result<(), String> {
1177    let tok = t.next_token()?.ok_or(format!("Expected '{kw}'"))?;
1178    if !tok.eq_ignore_ascii_case(kw) {
1179        return Err(format!("Expected '{kw}' but got '{tok}'"));
1180    }
1181    Ok(())
1182}
1183
1184// ---------------------------------------------------------------------------
1185// Simple tokenizer for PartiQL
1186// ---------------------------------------------------------------------------
1187
1188struct Tokenizer {
1189    tokens: Vec<String>,
1190    pos: usize,
1191    param_counter: usize,
1192}
1193
1194impl Tokenizer {
1195    fn new(input: &str) -> Result<Self, String> {
1196        let tokens = tokenize(input)?;
1197        Ok(Self {
1198            tokens,
1199            pos: 0,
1200            param_counter: 0,
1201        })
1202    }
1203
1204    fn next_token(&mut self) -> Result<Option<String>, String> {
1205        if self.pos >= self.tokens.len() {
1206            return Ok(None);
1207        }
1208        let tok = self.tokens[self.pos].clone();
1209        self.pos += 1;
1210        Ok(Some(tok))
1211    }
1212
1213    fn peek_token(&self) -> Result<Option<String>, String> {
1214        if self.pos >= self.tokens.len() {
1215            return Ok(None);
1216        }
1217        Ok(Some(self.tokens[self.pos].clone()))
1218    }
1219
1220    /// Peek at a token at a given offset from the current position.
1221    fn peek_token_at(&self, offset: usize) -> Result<Option<String>, String> {
1222        let idx = self.pos + offset;
1223        if idx >= self.tokens.len() {
1224            return Ok(None);
1225        }
1226        Ok(Some(self.tokens[idx].clone()))
1227    }
1228
1229    fn next_param_index(&mut self) -> usize {
1230        let idx = self.param_counter;
1231        self.param_counter += 1;
1232        idx
1233    }
1234}
1235
1236/// Tokenise a PartiQL string into tokens.
1237fn tokenize(input: &str) -> Result<Vec<String>, String> {
1238    let mut tokens = Vec::new();
1239    let chars: Vec<char> = input.chars().collect();
1240    let len = chars.len();
1241    let mut i = 0;
1242
1243    while i < len {
1244        // Skip whitespace
1245        if chars[i].is_ascii_whitespace() {
1246            i += 1;
1247            continue;
1248        }
1249
1250        // Single-char tokens
1251        match chars[i] {
1252            '{' | '}' | '[' | ']' | '(' | ')' | ',' | ':' | '*' | '?' | '+' | '-' | '.' => {
1253                // Check for multi-char - or +  as start of number? No, treat as separate.
1254                tokens.push(chars[i].to_string());
1255                i += 1;
1256                continue;
1257            }
1258            _ => {}
1259        }
1260
1261        // Two-char operators
1262        if i + 1 < len {
1263            let two = format!("{}{}", chars[i], chars[i + 1]);
1264            match two.as_str() {
1265                "<>" | "<=" | ">=" | "!=" => {
1266                    tokens.push(two);
1267                    i += 2;
1268                    continue;
1269                }
1270                _ => {}
1271            }
1272        }
1273
1274        // Single-char operators
1275        if matches!(chars[i], '=' | '<' | '>') {
1276            tokens.push(chars[i].to_string());
1277            i += 1;
1278            continue;
1279        }
1280
1281        // String literal (single-quoted), with '' escape support
1282        if chars[i] == '\'' {
1283            let mut s = String::from('\'');
1284            i += 1;
1285            while i < len {
1286                if chars[i] == '\'' {
1287                    // Check for '' escape sequence
1288                    if i + 1 < len && chars[i + 1] == '\'' {
1289                        s.push('\'');
1290                        i += 2;
1291                    } else {
1292                        break; // end of string
1293                    }
1294                } else {
1295                    s.push(chars[i]);
1296                    i += 1;
1297                }
1298            }
1299            if i < len {
1300                s.push('\'');
1301                i += 1;
1302            }
1303            tokens.push(s);
1304            continue;
1305        }
1306
1307        // Double-quoted identifier, with "" escape support
1308        if chars[i] == '"' {
1309            let mut s = String::from('"');
1310            i += 1;
1311            while i < len {
1312                if chars[i] == '"' {
1313                    // Check for "" escape sequence
1314                    if i + 1 < len && chars[i + 1] == '"' {
1315                        s.push('"');
1316                        i += 2;
1317                    } else {
1318                        break; // end of identifier
1319                    }
1320                } else {
1321                    s.push(chars[i]);
1322                    i += 1;
1323                }
1324            }
1325            if i < len {
1326                s.push('"');
1327                i += 1;
1328            }
1329            tokens.push(s);
1330            continue;
1331        }
1332
1333        // Number
1334        if chars[i].is_ascii_digit() {
1335            let mut s = String::new();
1336            while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') {
1337                s.push(chars[i]);
1338                i += 1;
1339            }
1340            tokens.push(s);
1341            continue;
1342        }
1343
1344        // Identifier / keyword
1345        if chars[i].is_ascii_alphabetic() || chars[i] == '_' {
1346            let mut s = String::new();
1347            while i < len && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
1348                s.push(chars[i]);
1349                i += 1;
1350            }
1351            tokens.push(s);
1352            continue;
1353        }
1354
1355        // Unknown character — report an error rather than silently skipping
1356        return Err(format!("Unexpected character: '{}'", chars[i]));
1357    }
1358
1359    Ok(tokens)
1360}
1361
1362#[cfg(test)]
1363mod tests {
1364    use super::*;
1365
1366    #[test]
1367    fn test_parse_select_star() {
1368        let stmt = parse("SELECT * FROM \"TestTable\"").unwrap();
1369        match stmt {
1370            Statement::Select {
1371                table_name,
1372                projections,
1373                where_clause,
1374            } => {
1375                assert_eq!(table_name, "TestTable");
1376                assert!(projections.is_empty());
1377                assert!(where_clause.is_none());
1378            }
1379            _ => panic!("Expected SELECT"),
1380        }
1381    }
1382
1383    #[test]
1384    fn test_parse_select_with_where() {
1385        let stmt = parse("SELECT * FROM \"T\" WHERE pk = 'hello'").unwrap();
1386        match stmt {
1387            Statement::Select {
1388                where_clause: Some(wc),
1389                ..
1390            } => {
1391                assert_eq!(wc.groups[0].len(), 1);
1392                match &wc.groups[0][0] {
1393                    WhereCondition::Comparison(c) => {
1394                        assert_eq!(c.path, "pk");
1395                        assert_eq!(c.op, CompOp::Eq);
1396                    }
1397                    _ => panic!("Expected Comparison"),
1398                }
1399            }
1400            _ => panic!("Expected SELECT with WHERE"),
1401        }
1402    }
1403
1404    #[test]
1405    fn test_parse_select_with_projection() {
1406        let stmt = parse("SELECT name, age FROM \"Users\"").unwrap();
1407        match stmt {
1408            Statement::Select { projections, .. } => {
1409                assert_eq!(projections, vec!["name", "age"]);
1410            }
1411            _ => panic!("Expected SELECT"),
1412        }
1413    }
1414
1415    #[test]
1416    fn test_parse_insert() {
1417        let stmt =
1418            parse("INSERT INTO \"TestTable\" VALUE {'pk': 'key1', 'data': 'hello'}").unwrap();
1419        match stmt {
1420            Statement::Insert {
1421                table_name, item, ..
1422            } => {
1423                assert_eq!(table_name, "TestTable");
1424                assert_eq!(
1425                    item.get("pk"),
1426                    Some(&PartiqlValue::Literal(AttributeValue::S(
1427                        "key1".to_string()
1428                    )))
1429                );
1430                assert_eq!(
1431                    item.get("data"),
1432                    Some(&PartiqlValue::Literal(AttributeValue::S(
1433                        "hello".to_string()
1434                    )))
1435                );
1436            }
1437            _ => panic!("Expected INSERT"),
1438        }
1439    }
1440
1441    #[test]
1442    fn test_parse_update() {
1443        let stmt = parse("UPDATE \"T\" SET name = 'Bob' WHERE pk = 'k1'").unwrap();
1444        match stmt {
1445            Statement::Update {
1446                table_name,
1447                set_clauses,
1448                where_clause,
1449                ..
1450            } => {
1451                assert_eq!(table_name, "T");
1452                assert_eq!(set_clauses.len(), 1);
1453                assert_eq!(set_clauses[0].path, "name");
1454                assert!(where_clause.is_some());
1455            }
1456            _ => panic!("Expected UPDATE"),
1457        }
1458    }
1459
1460    #[test]
1461    fn test_parse_delete() {
1462        let stmt = parse("DELETE FROM \"T\" WHERE pk = 'k1'").unwrap();
1463        match stmt {
1464            Statement::Delete {
1465                table_name,
1466                where_clause,
1467                returning,
1468            } => {
1469                assert_eq!(table_name, "T");
1470                assert!(where_clause.is_some());
1471                assert!(returning.is_none());
1472            }
1473            _ => panic!("Expected DELETE"),
1474        }
1475    }
1476
1477    #[test]
1478    fn test_parse_delete_returning_all_old() {
1479        let stmt = parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING ALL OLD *").unwrap();
1480        match stmt {
1481            Statement::Delete {
1482                table_name,
1483                where_clause,
1484                returning,
1485            } => {
1486                assert_eq!(table_name, "T");
1487                assert!(where_clause.is_some());
1488                assert_eq!(returning, Some(ReturningVariant::AllOld));
1489            }
1490            _ => panic!("Expected DELETE"),
1491        }
1492    }
1493
1494    #[test]
1495    fn test_parse_delete_returning_is_case_insensitive() {
1496        let stmt = parse("DELETE FROM \"T\" WHERE pk = 'k1' returning all old *").unwrap();
1497        match stmt {
1498            Statement::Delete { returning, .. } => {
1499                assert_eq!(returning, Some(ReturningVariant::AllOld))
1500            }
1501            _ => panic!("Expected DELETE"),
1502        }
1503    }
1504
1505    #[test]
1506    fn test_parse_delete_returning_recognises_all_variants() {
1507        // All four well-formed variants parse; DynamoDB's semantic rule (only
1508        // ALL OLD is valid on DELETE) is enforced by the executor so the
1509        // rejection can carry the exact validation message.
1510        let cases = [
1511            ("RETURNING ALL OLD *", ReturningVariant::AllOld),
1512            ("RETURNING MODIFIED OLD *", ReturningVariant::ModifiedOld),
1513            ("RETURNING ALL NEW *", ReturningVariant::AllNew),
1514            ("RETURNING MODIFIED NEW *", ReturningVariant::ModifiedNew),
1515        ];
1516        for (clause, expected) in cases {
1517            let stmt = parse(&format!("DELETE FROM \"T\" WHERE pk = 'k1' {clause}")).unwrap();
1518            match stmt {
1519                Statement::Delete { returning, .. } => {
1520                    assert_eq!(returning, Some(expected), "clause {clause}")
1521                }
1522                _ => panic!("Expected DELETE for {clause}"),
1523            }
1524        }
1525    }
1526
1527    #[test]
1528    fn test_parse_update_returning_recognises_all_variants() {
1529        let cases = [
1530            ("RETURNING ALL OLD *", ReturningVariant::AllOld),
1531            ("RETURNING MODIFIED OLD *", ReturningVariant::ModifiedOld),
1532            ("RETURNING ALL NEW *", ReturningVariant::AllNew),
1533            ("RETURNING MODIFIED NEW *", ReturningVariant::ModifiedNew),
1534        ];
1535        for (clause, expected) in cases {
1536            let stmt = parse(&format!(
1537                "UPDATE \"T\" SET data = 'x' WHERE pk = 'k1' {clause}"
1538            ))
1539            .unwrap();
1540            match stmt {
1541                Statement::Update { returning, .. } => {
1542                    assert_eq!(returning, Some(expected), "clause {clause}")
1543                }
1544                _ => panic!("Expected UPDATE for {clause}"),
1545            }
1546        }
1547    }
1548
1549    #[test]
1550    fn test_parse_delete_returning_rejects_incomplete_clause() {
1551        assert!(parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING ALL OLD").is_err());
1552        assert!(parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING").is_err());
1553        assert!(parse("DELETE FROM \"T\" WHERE pk = 'k1' RETURNING ALL OLD attr").is_err());
1554    }
1555
1556    #[test]
1557    fn test_parse_returning_rejects_invalid_keyword_pairs() {
1558        // Well-formed shape (two keywords + star) but not one of the four valid
1559        // ALL/MODIFIED x OLD/NEW pairs -> the catch-all rejection.
1560        for clause in [
1561            "RETURNING OLD ALL *",
1562            "RETURNING NEW MODIFIED *",
1563            "RETURNING ALL ALL *",
1564            "RETURNING FOO BAR *",
1565        ] {
1566            let err = parse(&format!("DELETE FROM \"T\" WHERE pk = 'k1' {clause}")).unwrap_err();
1567            assert!(
1568                err.contains("Unsupported RETURNING clause"),
1569                "clause {clause} gave unexpected error: {err}"
1570            );
1571        }
1572    }
1573
1574    #[test]
1575    fn test_parse_parameter() {
1576        let stmt = parse("SELECT * FROM \"T\" WHERE pk = ?").unwrap();
1577        match stmt {
1578            Statement::Select {
1579                where_clause: Some(wc),
1580                ..
1581            } => match &wc.groups[0][0] {
1582                WhereCondition::Comparison(c) => match &c.value {
1583                    PartiqlValue::Parameter(0) => {}
1584                    other => panic!("Expected Parameter(0), got {other:?}"),
1585                },
1586                _ => panic!("Expected Comparison"),
1587            },
1588            _ => panic!("Expected SELECT with WHERE"),
1589        }
1590    }
1591
1592    #[test]
1593    fn test_parse_numeric_literal() {
1594        let stmt = parse("SELECT * FROM \"T\" WHERE age > 42").unwrap();
1595        match stmt {
1596            Statement::Select {
1597                where_clause: Some(wc),
1598                ..
1599            } => match &wc.groups[0][0] {
1600                WhereCondition::Comparison(c) => {
1601                    assert_eq!(c.op, CompOp::Gt);
1602                    match &c.value {
1603                        PartiqlValue::Literal(AttributeValue::N(n)) => assert_eq!(n, "42"),
1604                        other => panic!("Expected N(42), got {other:?}"),
1605                    }
1606                }
1607                _ => panic!("Expected Comparison"),
1608            },
1609            _ => panic!("Expected SELECT"),
1610        }
1611    }
1612
1613    #[test]
1614    fn test_parse_insert_with_number() {
1615        let stmt = parse("INSERT INTO \"T\" VALUE {'pk': 'k1', 'age': 25}").unwrap();
1616        match stmt {
1617            Statement::Insert { item, .. } => {
1618                assert_eq!(
1619                    item.get("age"),
1620                    Some(&PartiqlValue::Literal(AttributeValue::N("25".to_string())))
1621                );
1622            }
1623            _ => panic!("Expected INSERT"),
1624        }
1625    }
1626
1627    #[test]
1628    fn test_invalid_statement() {
1629        let result = parse("MERGE INTO \"T\"");
1630        assert!(result.is_err());
1631    }
1632
1633    #[test]
1634    fn test_empty_statement() {
1635        let result = parse("");
1636        assert!(result.is_err());
1637    }
1638
1639    #[test]
1640    fn test_parse_between() {
1641        let stmt = parse("SELECT * FROM \"T\" WHERE age BETWEEN 18 AND 65").unwrap();
1642        match stmt {
1643            Statement::Select {
1644                where_clause: Some(wc),
1645                ..
1646            } => {
1647                assert_eq!(wc.groups[0].len(), 1);
1648                match &wc.groups[0][0] {
1649                    WhereCondition::Between(path, low, high) => {
1650                        assert_eq!(path, "age");
1651                        match low {
1652                            PartiqlValue::Literal(AttributeValue::N(n)) => assert_eq!(n, "18"),
1653                            other => panic!("Expected N(18), got {other:?}"),
1654                        }
1655                        match high {
1656                            PartiqlValue::Literal(AttributeValue::N(n)) => assert_eq!(n, "65"),
1657                            other => panic!("Expected N(65), got {other:?}"),
1658                        }
1659                    }
1660                    other => panic!("Expected Between, got {other:?}"),
1661                }
1662            }
1663            _ => panic!("Expected SELECT with WHERE"),
1664        }
1665    }
1666
1667    #[test]
1668    fn test_parse_between_and_other_condition() {
1669        let stmt = parse("SELECT * FROM \"T\" WHERE x BETWEEN 1 AND 10 AND y = 'hello'").unwrap();
1670        match stmt {
1671            Statement::Select {
1672                where_clause: Some(wc),
1673                ..
1674            } => {
1675                assert_eq!(wc.groups[0].len(), 2);
1676                assert!(matches!(&wc.groups[0][0], WhereCondition::Between(..)));
1677                assert!(matches!(&wc.groups[0][1], WhereCondition::Comparison(..)));
1678            }
1679            _ => panic!("Expected SELECT with WHERE"),
1680        }
1681    }
1682
1683    #[test]
1684    fn test_parse_in() {
1685        let stmt = parse("SELECT * FROM \"T\" WHERE status IN ('ACTIVE', 'PENDING')").unwrap();
1686        match stmt {
1687            Statement::Select {
1688                where_clause: Some(wc),
1689                ..
1690            } => {
1691                assert_eq!(wc.groups[0].len(), 1);
1692                match &wc.groups[0][0] {
1693                    WhereCondition::In(path, values) => {
1694                        assert_eq!(path, "status");
1695                        assert_eq!(values.len(), 2);
1696                    }
1697                    other => panic!("Expected In, got {other:?}"),
1698                }
1699            }
1700            _ => panic!("Expected SELECT with WHERE"),
1701        }
1702    }
1703
1704    #[test]
1705    fn test_parse_contains() {
1706        let stmt = parse("SELECT * FROM \"T\" WHERE CONTAINS(name, 'john')").unwrap();
1707        match stmt {
1708            Statement::Select {
1709                where_clause: Some(wc),
1710                ..
1711            } => {
1712                assert_eq!(wc.groups[0].len(), 1);
1713                match &wc.groups[0][0] {
1714                    WhereCondition::Contains(path, val) => {
1715                        assert_eq!(path, "name");
1716                        match val {
1717                            PartiqlValue::Literal(AttributeValue::S(s)) => {
1718                                assert_eq!(s, "john")
1719                            }
1720                            other => panic!("Expected S(john), got {other:?}"),
1721                        }
1722                    }
1723                    other => panic!("Expected Contains, got {other:?}"),
1724                }
1725            }
1726            _ => panic!("Expected SELECT with WHERE"),
1727        }
1728    }
1729
1730    #[test]
1731    fn test_parse_is_missing() {
1732        let stmt = parse("SELECT * FROM \"T\" WHERE email IS MISSING").unwrap();
1733        match stmt {
1734            Statement::Select {
1735                where_clause: Some(wc),
1736                ..
1737            } => {
1738                assert_eq!(wc.groups[0].len(), 1);
1739                match &wc.groups[0][0] {
1740                    WhereCondition::IsMissing(path) => assert_eq!(path, "email"),
1741                    other => panic!("Expected IsMissing, got {other:?}"),
1742                }
1743            }
1744            _ => panic!("Expected SELECT with WHERE"),
1745        }
1746    }
1747
1748    #[test]
1749    fn test_parse_is_not_missing() {
1750        let stmt = parse("SELECT * FROM \"T\" WHERE email IS NOT MISSING").unwrap();
1751        match stmt {
1752            Statement::Select {
1753                where_clause: Some(wc),
1754                ..
1755            } => {
1756                assert_eq!(wc.groups[0].len(), 1);
1757                match &wc.groups[0][0] {
1758                    WhereCondition::IsNotMissing(path) => assert_eq!(path, "email"),
1759                    other => panic!("Expected IsNotMissing, got {other:?}"),
1760                }
1761            }
1762            _ => panic!("Expected SELECT with WHERE"),
1763        }
1764    }
1765
1766    #[test]
1767    fn test_parse_nested_projection() {
1768        let stmt = parse("SELECT a.b.c, d FROM \"T\"").unwrap();
1769        match stmt {
1770            Statement::Select { projections, .. } => {
1771                assert_eq!(projections, vec!["a.b.c", "d"]);
1772            }
1773            _ => panic!("Expected SELECT"),
1774        }
1775    }
1776
1777    #[test]
1778    fn test_parse_array_index_projection() {
1779        let stmt = parse("SELECT items[0].name FROM \"T\"").unwrap();
1780        match stmt {
1781            Statement::Select { projections, .. } => {
1782                assert_eq!(projections, vec!["items[0].name"]);
1783            }
1784            _ => panic!("Expected SELECT"),
1785        }
1786    }
1787
1788    #[test]
1789    fn test_parse_update_with_remove() {
1790        let stmt =
1791            parse("UPDATE \"T\" SET name = 'Bob' REMOVE age, email WHERE pk = 'k1'").unwrap();
1792        match stmt {
1793            Statement::Update {
1794                set_clauses,
1795                remove_paths,
1796                where_clause,
1797                ..
1798            } => {
1799                assert_eq!(set_clauses.len(), 1);
1800                assert_eq!(remove_paths, vec!["age", "email"]);
1801                assert!(where_clause.is_some());
1802            }
1803            _ => panic!("Expected UPDATE"),
1804        }
1805    }
1806
1807    #[test]
1808    fn test_parse_update_remove_only() {
1809        let stmt = parse("UPDATE \"T\" REMOVE old_field WHERE pk = 'k1'").unwrap();
1810        match stmt {
1811            Statement::Update {
1812                set_clauses,
1813                remove_paths,
1814                ..
1815            } => {
1816                assert!(set_clauses.is_empty());
1817                assert_eq!(remove_paths, vec!["old_field"]);
1818            }
1819            _ => panic!("Expected UPDATE"),
1820        }
1821    }
1822
1823    #[test]
1824    fn test_parse_set_expression_add() {
1825        let stmt = parse("UPDATE \"T\" SET count = count + 1 WHERE pk = 'k1'").unwrap();
1826        match stmt {
1827            Statement::Update { set_clauses, .. } => {
1828                assert_eq!(set_clauses.len(), 1);
1829                match &set_clauses[0].value {
1830                    SetValue::Add(attr, val) => {
1831                        assert_eq!(attr, "count");
1832                        assert_eq!(
1833                            val,
1834                            &PartiqlValue::Literal(AttributeValue::N("1".to_string()))
1835                        );
1836                    }
1837                    other => panic!("Expected Add, got {other:?}"),
1838                }
1839            }
1840            _ => panic!("Expected UPDATE"),
1841        }
1842    }
1843
1844    #[test]
1845    fn test_parse_count_star() {
1846        let stmt = parse("SELECT COUNT(*) FROM \"T\"").unwrap();
1847        match stmt {
1848            Statement::Select { projections, .. } => {
1849                assert_eq!(projections, vec!["COUNT(*)"]);
1850            }
1851            _ => panic!("Expected SELECT"),
1852        }
1853    }
1854
1855    #[test]
1856    fn test_parse_set_literal() {
1857        let stmt = parse("INSERT INTO \"T\" VALUE {'pk': 'k1', 'tags': <<'a', 'b'>>}").unwrap();
1858        match stmt {
1859            Statement::Insert { item, .. } => match item.get("tags") {
1860                Some(PartiqlValue::Literal(AttributeValue::SS(ss))) => {
1861                    assert!(ss.contains(&"a".to_string()));
1862                    assert!(ss.contains(&"b".to_string()));
1863                }
1864                other => panic!("Expected SS, got {other:?}"),
1865            },
1866            _ => panic!("Expected INSERT"),
1867        }
1868    }
1869
1870    #[test]
1871    fn test_parse_or_condition() {
1872        let stmt = parse("SELECT * FROM \"T\" WHERE status = 'A' OR status = 'B'").unwrap();
1873        match stmt {
1874            Statement::Select {
1875                where_clause: Some(wc),
1876                ..
1877            } => {
1878                assert_eq!(wc.groups.len(), 2);
1879                assert_eq!(wc.groups[0].len(), 1);
1880                assert_eq!(wc.groups[1].len(), 1);
1881            }
1882            _ => panic!("Expected SELECT with WHERE"),
1883        }
1884    }
1885
1886    #[test]
1887    fn test_parse_and_or_mixed() {
1888        let stmt = parse("SELECT * FROM \"T\" WHERE a = 1 AND b = 2 OR c = 3").unwrap();
1889        match stmt {
1890            Statement::Select {
1891                where_clause: Some(wc),
1892                ..
1893            } => {
1894                assert_eq!(wc.groups.len(), 2);
1895                assert_eq!(wc.groups[0].len(), 2); // a = 1 AND b = 2
1896                assert_eq!(wc.groups[1].len(), 1); // c = 3
1897            }
1898            _ => panic!("Expected SELECT with WHERE"),
1899        }
1900    }
1901
1902    #[test]
1903    fn test_parse_insert_if_not_exists() {
1904        let stmt =
1905            parse("INSERT INTO \"T\" VALUE {'pk': 'k1', 'name': 'A'} IF NOT EXISTS").unwrap();
1906        match stmt {
1907            Statement::Insert { if_not_exists, .. } => {
1908                assert!(if_not_exists);
1909            }
1910            _ => panic!("Expected INSERT"),
1911        }
1912    }
1913
1914    #[test]
1915    fn test_parse_nested_path_in_where_function() {
1916        let stmt = parse("SELECT * FROM \"T\" WHERE BEGINS_WITH(address.city, 'Lon')").unwrap();
1917        match stmt {
1918            Statement::Select {
1919                where_clause: Some(wc),
1920                ..
1921            } => match &wc.groups[0][0] {
1922                WhereCondition::BeginsWith(path, _) => {
1923                    assert_eq!(path, "address.city");
1924                }
1925                other => panic!("Expected BeginsWith, got {other:?}"),
1926            },
1927            _ => panic!("Expected SELECT with WHERE"),
1928        }
1929    }
1930}