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