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