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