Skip to main content

reddb_rql/parser/
dml.rs

1//! DML SQL Parser: INSERT, UPDATE, DELETE
2
3use super::error::ParseError;
4use super::Parser;
5use crate::ast::{
6    AskCacheClause, AskQuery, BinOp, DeleteQuery, Expr, FieldRef, Filter, InsertEntityType,
7    InsertQuery, OrderByClause, QueryExpr, ReturningItem, UpdateQuery, UpdateTarget,
8};
9use crate::lexer::Token;
10use crate::sql_lowering::{filter_to_expr, fold_expr_to_value};
11use reddb_types::types::Value;
12
13/// Maximum nesting depth for JSON object literals — shared constant
14/// that now lives in the crate-level [`crate::limits`] module so every
15/// depth cap is co-located with [`crate::limits::ParserLimits`].
16pub(crate) use crate::limits::JSON_LITERAL_MAX_DEPTH;
17
18/// Walk a parsed `JsonValue` tree and bail out if nesting exceeds
19/// `JSON_LITERAL_MAX_DEPTH`. Iterative to avoid the very stack
20/// overflow we're trying to prevent.
21pub(crate) fn json_literal_depth_check(
22    value: &reddb_types::utils::json::JsonValue,
23) -> Result<(), String> {
24    use reddb_types::utils::json::JsonValue;
25    let mut stack: Vec<(&JsonValue, u32)> = vec![(value, 1)];
26    while let Some((node, depth)) = stack.pop() {
27        if depth > JSON_LITERAL_MAX_DEPTH {
28            return Err(format!(
29                "JSON object literal exceeds JSON_LITERAL_MAX_DEPTH ({})",
30                JSON_LITERAL_MAX_DEPTH
31            ));
32        }
33        match node {
34            JsonValue::Object(entries) => {
35                for (_, v) in entries {
36                    stack.push((v, depth + 1));
37                }
38            }
39            JsonValue::Array(items) => {
40                for v in items {
41                    stack.push((v, depth + 1));
42                }
43            }
44            _ => {}
45        }
46    }
47    Ok(())
48}
49
50impl<'a> Parser<'a> {
51    /// Parse: INSERT INTO table [NODE|EDGE|VECTOR|DOCUMENT|KV] (col1, col2) VALUES (val1, val2), (val3, val4) [RETURNING]
52    pub fn parse_insert_query(&mut self) -> Result<QueryExpr, ParseError> {
53        self.expect(Token::Insert)?;
54        self.expect(Token::Into)?;
55        // Issue #789 — Analytics v0 explicitly excludes `INSERT INTO
56        // METRIC <path>` as a raw write path (PRD #782 non-goal). Raw
57        // samples land in ordinary RedDB collections; the metric
58        // descriptor catalog is reached through `CREATE METRIC` and
59        // `red.analytics.metrics`. Reject the form here before the
60        // identifier slot so the error names the actual reason, not a
61        // generic "expected identifier".
62        if matches!(self.peek(), Token::Metric) {
63            return Err(ParseError::new(
64                "INSERT INTO METRIC is not supported in Analytics v0 — \
65                 write raw samples into an ordinary TABLE/DOCUMENT \
66                 collection; the metric descriptor catalog is reached \
67                 via CREATE METRIC and red.analytics.metrics \
68                 (PRD #782 non-goal)",
69                self.position(),
70            ));
71        }
72        let table = self.expect_ident()?;
73
74        // Check for entity type keyword
75        let entity_type = match self.peek().clone() {
76            Token::Node => {
77                self.advance()?;
78                InsertEntityType::Node
79            }
80            Token::Edge => {
81                self.advance()?;
82                InsertEntityType::Edge
83            }
84            Token::Vector => {
85                self.advance()?;
86                InsertEntityType::Vector
87            }
88            Token::Document => {
89                self.advance()?;
90                InsertEntityType::Document
91            }
92            Token::Kv => {
93                self.advance()?;
94                InsertEntityType::Kv
95            }
96            _ => InsertEntityType::Row,
97        };
98
99        // Parse column list. ADR 0067 (#1709): the document INSERT column
100        // list is removed — the canonical form is bare
101        // `VALUES (<json-literal>)`. For documents we synthesize the
102        // implicit `body` column so the runtime document path is unchanged,
103        // and reject the legacy `(body)` / `_ttl` column lists with a
104        // didactic error. Every other entity type keeps its mandatory
105        // column list.
106        let columns = if matches!(entity_type, InsertEntityType::Document) {
107            self.parse_document_insert_columns()?
108        } else if matches!(entity_type, InsertEntityType::Row) && self.check(&Token::Values) {
109            // ADR 0067 (#1710): the bare `INSERT INTO c VALUES (…)` form
110            // carries no column list and no model marker. The model is
111            // inferred from the catalog at analysis time — an existing
112            // document collection routes to document creation — so the
113            // parser leaves the column list empty and defers the routing
114            // decision to the runtime.
115            Vec::new()
116        } else {
117            self.expect(Token::LParen)?;
118            let columns = self.parse_ident_list()?;
119            self.expect(Token::RParen)?;
120            columns
121        };
122
123        // Parse VALUES
124        self.expect(Token::Values)?;
125        let mut all_values = Vec::new();
126        let mut all_value_exprs = Vec::new();
127        loop {
128            self.expect(Token::LParen)?;
129            let row_exprs = self.parse_dml_expr_list()?;
130            self.expect(Token::RParen)?;
131            // Tolerate `$N` / `?` placeholders in VALUES rows: fold to
132            // Value::Null and rely on `user_params::bind` to substitute
133            // the caller's values before execution. Issue #355.
134            // Tolerate `$N` / `?` placeholders in VALUES rows: if fold
135            // fails on an expression that contains `Expr::Parameter`,
136            // emit a `Value::Null` placeholder. `user_params::bind`
137            // substitutes the caller-supplied value before execution.
138            // Issue #355.
139            let row_values = row_exprs
140                .iter()
141                .map(|expr| match fold_expr_to_value(expr.clone()) {
142                    Ok(value) => Ok(value),
143                    Err(msg) => {
144                        if crate::sql_lowering::expr_contains_parameter(expr) {
145                            Ok(Value::Null)
146                        } else {
147                            Err(msg)
148                        }
149                    }
150                })
151                .collect::<Result<Vec<_>, _>>()
152                .map_err(|msg| ParseError::new(msg, self.position()))?;
153            all_value_exprs.push(row_exprs);
154            all_values.push(row_values);
155            if !self.consume(&Token::Comma)? {
156                break;
157            }
158        }
159
160        // ADR 0067 (#1709): a document body is an inline strict-JSON
161        // literal. Reject the removed quoted-string coercion
162        // (`VALUES ('{…}')`) with a didactic error naming the inline
163        // literal and `JSON_PARSE`.
164        if matches!(entity_type, InsertEntityType::Document) {
165            self.reject_document_string_body(&all_value_exprs)?;
166        }
167
168        // Parse optional WITH clauses
169        let (ttl_ms, expires_at_ms, with_metadata, auto_embed) = self.parse_with_clauses()?;
170
171        let returning = self.parse_returning_clause()?;
172
173        let suppress_events = if self.consume_ident_ci("SUPPRESS")? {
174            self.expect_ident_ci("EVENTS")?;
175            true
176        } else {
177            false
178        };
179
180        Ok(QueryExpr::Insert(InsertQuery {
181            table,
182            entity_type,
183            columns,
184            value_exprs: all_value_exprs,
185            values: all_values,
186            returning,
187            ttl_ms,
188            expires_at_ms,
189            with_metadata,
190            auto_embed,
191            suppress_events,
192        }))
193    }
194
195    /// ADR 0067 (#1709): the document INSERT column list is dead. The
196    /// canonical form is bare `VALUES (<json-literal>)`, so a document
197    /// INSERT carries no column list and we synthesize the implicit
198    /// `body` column here for the runtime document path. A leftover `(…)`
199    /// column list is rejected with a didactic error: `_ttl` metadata
200    /// columns point at `WITH TTL`; anything else (including the old
201    /// ceremonial `body`) shows the bare `VALUES` form.
202    fn parse_document_insert_columns(&mut self) -> Result<Vec<String>, ParseError> {
203        if !self.check(&Token::LParen) {
204            return Ok(vec!["body".to_string()]);
205        }
206        self.expect(Token::LParen)?;
207        let columns = self.parse_ident_list()?;
208        self.expect(Token::RParen)?;
209        if columns.iter().any(|column| is_legacy_ttl_column(column)) {
210            return Err(ParseError::document_insert_ttl_column(self.position()));
211        }
212        Err(ParseError::document_insert_column_list(self.position()))
213    }
214
215    /// ADR 0067 (#1709): a document body must be an inline strict-JSON
216    /// literal. A bare quoted string literal in a document `VALUES` row is
217    /// the removed coercion (`VALUES ('{…}')`) and is rejected with a
218    /// didactic error. Parameters (`$N` / `?`) and `JSON_PARSE(<expr>)`
219    /// remain valid, so only `Expr::Literal { Value::Text }` is rejected.
220    fn reject_document_string_body(&self, value_exprs: &[Vec<Expr>]) -> Result<(), ParseError> {
221        for row in value_exprs {
222            for expr in row {
223                if let Expr::Literal {
224                    value: Value::Text(_),
225                    ..
226                } = expr
227                {
228                    return Err(ParseError::document_insert_quoted_body(self.position()));
229                }
230            }
231        }
232        Ok(())
233    }
234
235    /// Parse TTL duration value using the same logic as CREATE TABLE ... WITH TTL.
236    fn parse_ttl_duration(&mut self) -> Result<u64, ParseError> {
237        // Reuse the DDL TTL parser: expects a number followed by optional unit
238        let ttl_value = self.parse_float()?;
239        let ttl_unit = match self.peek() {
240            Token::Ident(unit) => {
241                let unit = unit.clone();
242                self.advance()?;
243                unit
244            }
245            _ => "s".to_string(),
246        };
247
248        let multiplier_ms = match ttl_unit.to_ascii_lowercase().as_str() {
249            "ms" | "msec" | "millisecond" | "milliseconds" => 1.0,
250            "s" | "sec" | "secs" | "second" | "seconds" => 1_000.0,
251            "m" | "min" | "mins" | "minute" | "minutes" => 60_000.0,
252            "h" | "hr" | "hrs" | "hour" | "hours" => 3_600_000.0,
253            "d" | "day" | "days" => 86_400_000.0,
254            other => {
255                return Err(ParseError::new(
256                    // F-05: render `other` via `{:?}` so caller-controlled
257                    // bytes (CR / LF / NUL / quotes) are escaped before
258                    // landing in the JSON/audit/log/gRPC error sinks.
259                    format!(
260                        "unsupported TTL unit {other:?}; supported units: ms, s, m, h, d (e.g. `WITH TTL 30 m`)"
261                    ),
262                    self.position(),
263                ));
264            }
265        };
266
267        Ok((ttl_value * multiplier_ms) as u64)
268    }
269
270    /// Parse WITH clauses: WITH TTL | EXPIRES AT | METADATA | AUTO EMBED
271    /// Returns (ttl_ms, expires_at_ms, metadata, auto_embed)
272    pub fn parse_with_clauses(
273        &mut self,
274    ) -> Result<
275        (
276            Option<u64>,
277            Option<u64>,
278            Vec<(String, Value)>,
279            Option<crate::ast::AutoEmbedConfig>,
280        ),
281        ParseError,
282    > {
283        let mut ttl_ms = None;
284        let mut expires_at_ms = None;
285        let mut with_metadata = Vec::new();
286        let mut auto_embed = None;
287
288        while self.consume(&Token::With)? {
289            if self.consume_ident_ci("TTL")? {
290                ttl_ms = Some(self.parse_ttl_duration()?);
291            } else if self.consume_ident_ci("EXPIRES")? {
292                self.expect_ident_ci("AT")?;
293                let ts = self.parse_expires_at_value()?;
294                expires_at_ms = Some(ts);
295            } else if self.consume(&Token::Metadata)? || self.consume_ident_ci("METADATA")? {
296                with_metadata = self.parse_with_metadata_pairs()?;
297            } else if self.consume_ident_ci("AUTO")? {
298                // WITH AUTO EMBED (field1, field2) [USING provider] [MODEL 'model']
299                self.consume_ident_ci("EMBED")?;
300                self.expect(Token::LParen)?;
301                let mut fields = Vec::new();
302                loop {
303                    fields.push(self.expect_ident()?);
304                    if !self.consume(&Token::Comma)? {
305                        break;
306                    }
307                }
308                self.expect(Token::RParen)?;
309                // `USING` is a reserved keyword (`Token::Using`), so
310                // `consume_ident_ci` would never match. Use the typed
311                // consumer instead. See bug #108 (mirrors the #92 fix
312                // for migration `DEPENDS ON`).
313                let provider = if self.consume(&Token::Using)? {
314                    self.expect_ident()?
315                } else {
316                    "openai".to_string()
317                };
318                let model = if self.consume_ident_ci("MODEL")? {
319                    Some(self.parse_string()?)
320                } else {
321                    None
322                };
323                auto_embed = Some(crate::ast::AutoEmbedConfig {
324                    fields,
325                    provider,
326                    model,
327                });
328            } else {
329                return Err(ParseError::expected(
330                    vec!["TTL", "EXPIRES AT", "METADATA", "AUTO EMBED"],
331                    self.peek(),
332                    self.position(),
333                ));
334            }
335        }
336
337        Ok((ttl_ms, expires_at_ms, with_metadata, auto_embed))
338    }
339
340    /// Expect a case-insensitive identifier (error if not found)
341    fn expect_ident_ci(&mut self, expected: &str) -> Result<(), ParseError> {
342        if self.consume_ident_ci(expected)? {
343            Ok(())
344        } else {
345            Err(ParseError::expected(
346                vec![expected],
347                self.peek(),
348                self.position(),
349            ))
350        }
351    }
352
353    /// Parse an absolute expiration timestamp (unix ms or string date)
354    fn parse_expires_at_value(&mut self) -> Result<u64, ParseError> {
355        // Try integer (unix timestamp in ms)
356        if let Ok(value) = self.parse_integer() {
357            return Ok(value as u64);
358        }
359        // Try string like '2026-12-31' — convert to unix ms
360        if let Ok(text) = self.parse_string() {
361            // Simple ISO date parsing: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS
362            let trimmed = text.trim();
363            if let Ok(ts) = trimmed.parse::<u64>() {
364                return Ok(ts);
365            }
366            // Basic date parsing — delegate to chrono if available, or simple heuristic
367            return Err(ParseError::new(
368                // F-05: `trimmed` is caller-controlled string-literal bytes.
369                // Render via `{:?}` so CR/LF/NUL/quotes are escaped before
370                // the message reaches the JSON / audit / log / gRPC sinks.
371                format!("EXPIRES AT requires a unix timestamp in milliseconds, got {trimmed:?}"),
372                self.position(),
373            ));
374        }
375        Err(ParseError::expected(
376            vec!["timestamp (unix ms) or 'YYYY-MM-DD'"],
377            self.peek(),
378            self.position(),
379        ))
380    }
381
382    /// Parse WITH METADATA (key1 = 'value1', key2 = 42)
383    fn parse_with_metadata_pairs(&mut self) -> Result<Vec<(String, Value)>, ParseError> {
384        self.expect(Token::LParen)?;
385        let mut pairs = Vec::new();
386        if !self.check(&Token::RParen) {
387            loop {
388                let key = self.expect_ident_or_keyword()?.to_ascii_lowercase();
389                self.expect(Token::Eq)?;
390                let value = self.parse_literal_value()?;
391                pairs.push((key, value));
392                if !self.consume(&Token::Comma)? {
393                    break;
394                }
395            }
396        }
397        self.expect(Token::RParen)?;
398        Ok(pairs)
399    }
400
401    /// Parse: UPDATE table SET col1=val1, col2=val2 [WHERE filter] [WITH TTL|EXPIRES AT|METADATA]
402    pub fn parse_update_query(&mut self) -> Result<QueryExpr, ParseError> {
403        self.expect(Token::Update)?;
404        let table = self.expect_ident()?;
405        let target = self.parse_update_target()?;
406        self.expect(Token::Set)?;
407
408        let mut assignments = Vec::new();
409        let mut assignment_exprs = Vec::new();
410        let mut compound_assignment_ops = Vec::new();
411        loop {
412            let col = self.parse_update_assignment_target(target)?;
413            let compound_op = if self.consume(&Token::Eq)? {
414                None
415            } else {
416                let op = match self.peek() {
417                    Token::Plus => BinOp::Add,
418                    Token::Dash | Token::Minus => BinOp::Sub,
419                    Token::Star => BinOp::Mul,
420                    Token::Slash => BinOp::Div,
421                    Token::Percent => BinOp::Mod,
422                    _ => {
423                        return Err(ParseError::expected(
424                            vec!["=", "+=", "-=", "*=", "/=", "%="],
425                            self.peek(),
426                            self.position(),
427                        ));
428                    }
429                };
430                self.advance()?;
431                self.expect(Token::Eq)?;
432                Some(op)
433            };
434            let expr = self.parse_expr()?;
435            let folded = fold_expr_to_value(expr.clone()).ok();
436            assignment_exprs.push((col.clone(), expr));
437            compound_assignment_ops.push(compound_op);
438            if compound_op.is_none() {
439                if let Some(val) = folded {
440                    assignments.push((col.clone(), val));
441                }
442            }
443            if !self.consume(&Token::Comma)? {
444                break;
445            }
446        }
447
448        let filter = if self.consume(&Token::Where)? {
449            Some(self.parse_filter()?)
450        } else {
451            None
452        };
453        let where_expr = filter.as_ref().map(filter_to_expr);
454
455        let (ttl_ms, expires_at_ms, with_metadata, _auto_embed) = self.parse_with_clauses()?;
456
457        let (claim_limit, claim_exact) = if self.consume_ident_ci("CLAIM")? {
458            if self.consume_ident_ci("EXACT")? {
459                (Some(self.parse_integer()? as u64), true)
460            } else {
461                self.expect(Token::Limit)?;
462                (Some(self.parse_integer()? as u64), false)
463            }
464        } else {
465            (None, false)
466        };
467
468        let mut order_by = if self.consume(&Token::Order)? {
469            self.expect(Token::By)?;
470            let clauses = self.parse_order_by_list()?;
471            validate_update_order_by(&clauses, self.position())?;
472            clauses
473        } else {
474            Vec::new()
475        };
476
477        // Optional `LIMIT N` — used by `BATCH N ROWS` data migrations
478        // to cap a single batch. Must come after WHERE / WITH because
479        // those have their own keyword tokens that the LIMIT branch
480        // would otherwise mis-consume.
481        let limit = if self.consume(&Token::Limit)? {
482            Some(self.parse_integer()? as u64)
483        } else {
484            None
485        };
486        // CLAIM LIMIT acts as the LIMIT for the purpose of ORDER BY semantics:
487        // a claim without a conventional LIMIT still has a deterministic bound.
488        let effective_limit = limit.is_some() || claim_limit.is_some();
489        if !order_by.is_empty() && !effective_limit {
490            return Err(ParseError::new(
491                "UPDATE ORDER BY requires LIMIT",
492                self.position(),
493            ));
494        }
495        if claim_limit.is_some() && order_by.is_empty() {
496            return Err(ParseError::new(
497                "UPDATE CLAIM requires ORDER BY",
498                self.position(),
499            ));
500        }
501        if !order_by.is_empty() && !update_order_by_mentions_rid(&order_by) {
502            order_by.push(OrderByClause {
503                field: FieldRef::TableColumn {
504                    table: String::new(),
505                    column: "rid".to_string(),
506                },
507                expr: None,
508                ascending: true,
509                nulls_first: false,
510            });
511        }
512
513        let returning = self.parse_returning_clause()?;
514
515        let suppress_events = if self.consume_ident_ci("SUPPRESS")? {
516            self.expect_ident_ci("EVENTS")?;
517            true
518        } else {
519            false
520        };
521
522        Ok(QueryExpr::Update(UpdateQuery {
523            table,
524            target,
525            assignment_exprs,
526            compound_assignment_ops,
527            assignments,
528            where_expr,
529            filter,
530            ttl_ms,
531            expires_at_ms,
532            with_metadata,
533            returning,
534            claim_limit,
535            claim_exact,
536            order_by,
537            limit,
538            suppress_events,
539        }))
540    }
541
542    fn parse_update_assignment_target(
543        &mut self,
544        target: UpdateTarget,
545    ) -> Result<String, ParseError> {
546        let mut segments = vec![self.expect_column_ident()?];
547        if matches!(target, UpdateTarget::Documents) {
548            while self.consume(&Token::Dot)? {
549                segments.push(self.expect_column_ident()?);
550            }
551        }
552        Ok(segments.join("."))
553    }
554
555    fn parse_update_target(&mut self) -> Result<UpdateTarget, ParseError> {
556        if self.consume(&Token::Kv)? {
557            return Ok(UpdateTarget::Kv);
558        }
559        if self.consume(&Token::Rows)? {
560            return Ok(UpdateTarget::Rows);
561        }
562        if self.consume_ident_ci("DOCUMENTS")? {
563            return Ok(UpdateTarget::Documents);
564        }
565        if self.consume_ident_ci("NODES")? {
566            return Ok(UpdateTarget::Nodes);
567        }
568        if self.consume_ident_ci("EDGES")? {
569            return Ok(UpdateTarget::Edges);
570        }
571        Ok(UpdateTarget::Rows)
572    }
573
574    /// Parse: DELETE FROM table [WHERE filter]
575    pub fn parse_delete_query(&mut self) -> Result<QueryExpr, ParseError> {
576        self.expect(Token::Delete)?;
577        self.expect(Token::From)?;
578        let table = self.expect_ident()?;
579
580        let filter = if self.consume(&Token::Where)? {
581            Some(self.parse_filter()?)
582        } else {
583            None
584        };
585
586        let where_expr = filter.as_ref().map(filter_to_expr);
587
588        let returning = self.parse_returning_clause()?;
589
590        let suppress_events = if self.consume_ident_ci("SUPPRESS")? {
591            self.expect_ident_ci("EVENTS")?;
592            true
593        } else {
594            false
595        };
596
597        Ok(QueryExpr::Delete(DeleteQuery {
598            table,
599            where_expr,
600            filter,
601            returning,
602            suppress_events,
603        }))
604    }
605
606    /// Parse optional `RETURNING (* | col [, col ...])` clause.
607    /// Returns `None` if no RETURNING token, errors if RETURNING is present
608    /// but not followed by `*` or a non-empty column list.
609    fn parse_returning_clause(&mut self) -> Result<Option<Vec<ReturningItem>>, ParseError> {
610        if !self.consume(&Token::Returning)? {
611            return Ok(None);
612        }
613        if self.consume(&Token::Star)? {
614            return Ok(Some(vec![ReturningItem::All]));
615        }
616        let mut items = Vec::new();
617        loop {
618            if returning_expr_start(self.peek()) {
619                return Err(returning_expr_not_supported(self.position()));
620            }
621            let col = self.expect_update_returning_column()?;
622            items.push(ReturningItem::Column(col));
623            if returning_expr_tail(self.peek()) {
624                return Err(returning_expr_not_supported(self.position()));
625            }
626            if !self.consume(&Token::Comma)? {
627                break;
628            }
629        }
630        if items.is_empty() {
631            return Err(ParseError::expected(
632                vec!["*", "column name"],
633                self.peek(),
634                self.position(),
635            ));
636        }
637        Ok(Some(items))
638    }
639
640    fn expect_update_returning_column(&mut self) -> Result<String, ParseError> {
641        if self.consume(&Token::Weight)? {
642            return Ok("weight".to_string());
643        }
644        self.expect_ident_or_keyword()
645    }
646
647    /// Parse: ASK 'question' [USING provider] [MODEL 'model'] [DEPTH n]
648    /// [LIMIT n] [MIN_SCORE x] [COLLECTION col] [AS RQL]
649    pub fn parse_ask_query(&mut self) -> Result<QueryExpr, ParseError> {
650        self.parse_ask_query_with_explain(false)
651    }
652
653    /// Parse: EXPLAIN ASK 'question' ...
654    pub fn parse_explain_ask_query(&mut self) -> Result<QueryExpr, ParseError> {
655        self.advance()?; // consume EXPLAIN
656        if !matches!(self.peek(), Token::Ident(name) if name.eq_ignore_ascii_case("ASK")) {
657            return Err(ParseError::expected(
658                vec!["ASK"],
659                self.peek(),
660                self.position(),
661            ));
662        }
663        self.parse_ask_query_with_explain(true)
664    }
665
666    fn parse_ask_query_with_explain(&mut self, explain: bool) -> Result<QueryExpr, ParseError> {
667        self.advance()?; // consume ASK
668
669        let (question, question_param) = match self.peek() {
670            Token::String(_) => (self.parse_string()?, None),
671            Token::Dollar | Token::Question => {
672                let index = self.parse_param_slot("ASK question")?;
673                (String::new(), Some(index))
674            }
675            other => {
676                return Err(ParseError::expected(
677                    vec!["string", "$N", "?"],
678                    other,
679                    self.position(),
680                ));
681            }
682        };
683
684        let mut provider = None;
685        let mut model = None;
686        let mut depth = None;
687        let mut limit = None;
688        let mut min_score = None;
689        let mut collection = None;
690        let mut temperature = None;
691        let mut seed = None;
692        let mut strict = true;
693        let mut stream = false;
694        let mut cache = AskCacheClause::Default;
695        let mut as_rql = false;
696        let mut execute = false;
697
698        // Parse optional clauses in any order. Loop bound = number of
699        // clause kinds, so each can appear at most once.
700        for _ in 0..14 {
701            if self.consume(&Token::Using)? {
702                provider = Some(match &self.current.token {
703                    Token::String(_) => self.parse_string()?,
704                    _ => self.expect_ident()?,
705                });
706            } else if self.consume_ident_ci("MODEL")? {
707                model = Some(self.parse_string()?);
708            } else if self.consume(&Token::Depth)? {
709                depth = Some(self.parse_integer()? as usize);
710            } else if self.consume(&Token::Limit)? {
711                limit = Some(self.parse_integer()? as usize);
712            } else if self.consume(&Token::MinScore)? {
713                min_score = Some(self.parse_float()? as f32);
714            } else if self.consume(&Token::Collection)? {
715                collection = Some(self.expect_ident()?);
716            } else if self.consume_ident_ci("TEMPERATURE")? {
717                temperature = Some(self.parse_float()? as f32);
718            } else if self.consume_ident_ci("SEED")? {
719                seed = Some(self.parse_integer()? as u64);
720            } else if self.consume_ident_ci("STRICT")? {
721                let value = self.expect_ident_or_keyword()?;
722                if value.eq_ignore_ascii_case("ON") {
723                    strict = true;
724                } else if value.eq_ignore_ascii_case("OFF") {
725                    strict = false;
726                } else {
727                    return Err(ParseError::new(
728                        "Expected ON or OFF after STRICT",
729                        self.position(),
730                    ));
731                }
732            } else if self.consume_ident_ci("STREAM")? {
733                stream = true;
734            } else if self.consume_ident_ci("CACHE")? {
735                if !matches!(cache, AskCacheClause::Default) {
736                    return Err(ParseError::new(
737                        "ASK cache clause specified more than once",
738                        self.position(),
739                    ));
740                }
741                let ttl = self.expect_ident_or_keyword()?;
742                if !ttl.eq_ignore_ascii_case("TTL") {
743                    return Err(ParseError::new("Expected TTL after CACHE", self.position()));
744                }
745                cache = AskCacheClause::CacheTtl(self.parse_string()?);
746            } else if self.consume_ident_ci("NOCACHE")? {
747                if !matches!(cache, AskCacheClause::Default) {
748                    return Err(ParseError::new(
749                        "ASK cache clause specified more than once",
750                        self.position(),
751                    ));
752                }
753                cache = AskCacheClause::NoCache;
754            } else if self.consume(&Token::As)? {
755                if as_rql {
756                    return Err(ParseError::new(
757                        "ASK AS RQL specified more than once",
758                        self.position(),
759                    ));
760                }
761                let output = self.expect_ident_or_keyword()?;
762                if !output.eq_ignore_ascii_case("RQL") {
763                    return Err(ParseError::new(
764                        "Expected RQL after ASK AS",
765                        self.position(),
766                    ));
767                }
768                as_rql = true;
769            } else if self.consume_ident_ci("EXECUTE")? {
770                if execute {
771                    return Err(ParseError::new(
772                        "ASK EXECUTE specified more than once",
773                        self.position(),
774                    ));
775                }
776                execute = true;
777            } else {
778                break;
779            }
780        }
781
782        Ok(QueryExpr::Ask(AskQuery {
783            explain,
784            question,
785            question_param,
786            provider,
787            model,
788            depth,
789            limit,
790            min_score,
791            collection,
792            temperature,
793            seed,
794            strict,
795            stream,
796            cache,
797            as_rql,
798            execute,
799        }))
800    }
801
802    /// Parse comma-separated identifiers (accepts keywords as column names in DML context)
803    fn parse_ident_list(&mut self) -> Result<Vec<String>, ParseError> {
804        let mut idents = Vec::new();
805        loop {
806            idents.push(self.expect_ident_or_keyword()?);
807            if !self.consume(&Token::Comma)? {
808                break;
809            }
810        }
811        Ok(idents)
812    }
813
814    /// Parse comma-separated literal values for DML statements
815    fn parse_dml_value_list(&mut self) -> Result<Vec<Value>, ParseError> {
816        self.parse_dml_expr_list()?
817            .into_iter()
818            .map(fold_expr_to_value)
819            .collect::<Result<Vec<_>, _>>()
820            .map_err(|msg| ParseError::new(msg, self.position()))
821    }
822
823    fn parse_dml_expr_list(&mut self) -> Result<Vec<Expr>, ParseError> {
824        let mut values = Vec::new();
825        loop {
826            values.push(self.parse_expr()?);
827            if !self.consume(&Token::Comma)? {
828                break;
829            }
830        }
831        Ok(values)
832    }
833
834    /// Parse a single literal value (string, number, true, false, null, array)
835    pub(crate) fn parse_literal_value(&mut self) -> Result<Value, ParseError> {
836        // Depth guard: this function recurses for nested array `[…]`
837        // and object `{…}` literals (see the LBracket / LBrace arms
838        // below). Without entering the depth counter, an adversarial
839        // payload like `[[[[…(10k×)…]]]]` would overflow the Rust
840        // stack BEFORE `ParserLimits::max_depth` fires. The
841        // `JsonLiteral` token path uses `json_literal_depth_check`
842        // (iterative) — the bare `[`/`{` path needs the recursion
843        // counter explicitly.
844        self.enter_depth()?;
845        let result = self.parse_literal_value_inner();
846        self.exit_depth();
847        result
848    }
849
850    fn parse_literal_value_inner(&mut self) -> Result<Value, ParseError> {
851        // Recognize PASSWORD('plaintext') and SECRET('plaintext') as
852        // typed literal constructors. The parser stores them as
853        // sentinel-prefixed values so that the INSERT executor can
854        // apply the crypto transform (argon2id hash / AES-256-GCM
855        // encrypt) without the parser depending on auth or crypto
856        // subsystems.
857        if let Token::Ident(name) = self.peek().clone() {
858            let upper = name.to_uppercase();
859            if upper == "PASSWORD" || upper == "SECRET" {
860                self.advance()?; // consume ident
861                self.expect(Token::LParen)?;
862                let plaintext = self.parse_string()?;
863                self.expect(Token::RParen)?;
864                return Ok(match upper.as_str() {
865                    "PASSWORD" => Value::Password(format!("@@plain@@{plaintext}")),
866                    "SECRET" => Value::Secret(format!("@@plain@@{plaintext}").into_bytes()),
867                    _ => unreachable!(),
868                });
869            }
870            if upper == "SECRET_REF" {
871                self.advance()?; // consume ident
872                self.expect(Token::LParen)?;
873                let store = self.expect_ident_or_keyword()?.to_ascii_lowercase();
874                if store != "vault" {
875                    return Err(ParseError::expected(
876                        vec!["vault"],
877                        self.peek(),
878                        self.position(),
879                    ));
880                }
881                self.expect(Token::Comma)?;
882                let (collection, key) =
883                    self.parse_kv_key(reddb_types::catalog::CollectionModel::Vault)?;
884                self.expect(Token::RParen)?;
885                return Ok(secret_ref_value(&store, &collection, &key));
886            }
887        }
888
889        match self.peek().clone() {
890            Token::String(s) => {
891                let s = s.clone();
892                self.advance()?;
893                Ok(Value::text(s))
894            }
895            Token::JsonLiteral(raw) => {
896                // The lexer already validated brace balance and the
897                // 16 MiB payload ceiling. Parse the raw text into a
898                // canonical JsonValue then re-encode via `to_vec` so
899                // the on-disk bytes match the quoted form.
900                self.advance()?;
901                let json_value = reddb_types::utils::json::parse_json(&raw).map_err(|err| {
902                    ParseError::new(
903                        // F-05: render the underlying parse-error string
904                        // via `{:?}` so any user fragment serde echoed
905                        // back (unexpected character, key text, …) is
906                        // Debug-escaped before reaching the downstream
907                        // JSON / audit / log / gRPC sinks.
908                        format!("invalid JSON object literal: {:?}", err.to_string()),
909                        self.position(),
910                    )
911                })?;
912                json_literal_depth_check(&json_value)
913                    .map_err(|err| ParseError::new(err, self.position()))?;
914                let canonical = reddb_types::serde_json::Value::from(json_value);
915                let bytes = reddb_types::json::to_vec(&canonical).map_err(|err| {
916                    ParseError::new(
917                        // F-05: escape the encoder error via `{:?}` so any
918                        // user fragment it carries cannot smuggle control
919                        // bytes through downstream serialization sinks.
920                        format!("failed to encode JSON literal: {:?}", err.to_string()),
921                        self.position(),
922                    )
923                })?;
924                Ok(Value::Json(bytes))
925            }
926            Token::Integer(n) => {
927                self.advance()?;
928                Ok(Value::Integer(n))
929            }
930            Token::Float(n) => {
931                self.advance()?;
932                Ok(Value::Float(n))
933            }
934            Token::True => {
935                self.advance()?;
936                Ok(Value::Boolean(true))
937            }
938            Token::False => {
939                self.advance()?;
940                Ok(Value::Boolean(false))
941            }
942            Token::Null => {
943                self.advance()?;
944                Ok(Value::Null)
945            }
946            Token::LBracket => {
947                // Parse array literal `[val1, val2, ...]` **losslessly** into a
948                // `Value::Array`, preserving each element's integer/float/other
949                // identity (issue #1708, ADR 0067). The parser deliberately does
950                // NOT decide here whether the array is a vector or a JSON array:
951                // committing to an f32 `Value::Vector` at parse time silently
952                // corrupts large integers destined for a JSON position (e.g.
953                // `[9007199254740993]`). Instead the analyzer/runtime resolves
954                // the concrete shape from the target's type — a vector-typed
955                // position coerces `Value::Array` → `Value::Vector`, a JSON
956                // position coerces it to an exact JSON array.
957                self.advance()?; // consume '['
958                let mut items = Vec::new();
959                if !self.check(&Token::RBracket) {
960                    loop {
961                        items.push(self.parse_literal_value()?);
962                        if !self.consume(&Token::Comma)? {
963                            break;
964                        }
965                    }
966                }
967                self.expect(Token::RBracket)?;
968                Ok(Value::Array(items))
969            }
970            Token::LBrace => {
971                // Parse JSON object literal {key: value, ...}
972                self.advance()?; // consume '{'
973                let mut map = reddb_types::json::Map::new();
974                if !self.check(&Token::RBrace) {
975                    loop {
976                        // Key: string or identifier. Reserved-word
977                        // keys (`level`, `msg`, `type`, …) fall through
978                        // to `expect_ident_or_keyword`, which returns
979                        // the canonical UPPERCASE spelling; lowercase
980                        // that path so the JSON object preserves the
981                        // source casing.
982                        let key = match self.peek().clone() {
983                            Token::String(s) => {
984                                self.advance()?;
985                                s
986                            }
987                            Token::Ident(s) => {
988                                self.advance()?;
989                                s
990                            }
991                            _ => self.expect_ident_or_keyword()?.to_ascii_lowercase(),
992                        };
993                        // Separator: ':' or '='
994                        if !self.consume(&Token::Colon)? {
995                            self.expect(Token::Eq)?;
996                        }
997                        // Value: recursive
998                        let val = self.parse_literal_value()?;
999                        map.insert(key, literal_value_to_json(&val));
1000                        if !self.consume(&Token::Comma)? {
1001                            break;
1002                        }
1003                    }
1004                }
1005                self.expect(Token::RBrace)?;
1006                let json_val = reddb_types::json::Value::Object(map);
1007                let bytes = reddb_types::json::to_vec(&json_val).unwrap_or_default();
1008                Ok(Value::Json(bytes))
1009            }
1010            ref other => Err(ParseError::expected(
1011                vec!["string", "number", "true", "false", "null", "[", "{"],
1012                other,
1013                self.position(),
1014            )),
1015        }
1016    }
1017}
1018
1019/// ADR 0067 (#1709): the legacy metadata columns whose presence in a
1020/// document INSERT column list should steer the didactic error toward
1021/// `WITH TTL`. Mirrors `resolve_sql_ttl_metadata_key` in the server
1022/// runtime; kept local so the parser has no server dependency.
1023fn is_legacy_ttl_column(column: &str) -> bool {
1024    column.eq_ignore_ascii_case("_ttl")
1025        || column.eq_ignore_ascii_case("_ttl_ms")
1026        || column.eq_ignore_ascii_case("_expires_at")
1027}
1028
1029/// Convert a parsed literal `Value` into a `reddb_types::json::Value`.
1030///
1031/// Array literals now parse into `Value::Array` (issue #1708). When an array
1032/// appears inside a JSON object-literal position (e.g.
1033/// `{roles: ['edge', 'cache']}`) it must serialise as a JSON array rather than
1034/// collapsing to `null` the way the old catch-all arm did. Recurses so nested
1035/// arrays and objects round-trip.
1036fn literal_value_to_json(val: &Value) -> reddb_types::json::Value {
1037    match val {
1038        Value::Null => reddb_types::json::Value::Null,
1039        Value::Boolean(b) => reddb_types::json::Value::Bool(*b),
1040        Value::Integer(i) => reddb_types::json::Value::Number(*i as f64),
1041        Value::Float(f) => reddb_types::json::Value::Number(*f),
1042        Value::Text(s) => reddb_types::json::Value::String(s.to_string()),
1043        Value::Json(bytes) => {
1044            reddb_types::json::from_slice(bytes).unwrap_or(reddb_types::json::Value::Null)
1045        }
1046        Value::Array(items) => {
1047            reddb_types::json::Value::Array(items.iter().map(literal_value_to_json).collect())
1048        }
1049        _ => reddb_types::json::Value::Null,
1050    }
1051}
1052
1053fn returning_expr_start(token: &Token) -> bool {
1054    matches!(
1055        token,
1056        Token::Integer(_)
1057            | Token::Float(_)
1058            | Token::String(_)
1059            | Token::JsonLiteral(_)
1060            | Token::Null
1061            | Token::True
1062            | Token::False
1063            | Token::LParen
1064            | Token::Minus
1065            | Token::Question
1066            | Token::Dollar
1067    )
1068}
1069
1070fn returning_expr_tail(token: &Token) -> bool {
1071    matches!(
1072        token,
1073        Token::LParen
1074            | Token::Plus
1075            | Token::Minus
1076            | Token::Star
1077            | Token::Slash
1078            | Token::Percent
1079            | Token::DoublePipe
1080            | Token::Pipe
1081            | Token::Eq
1082            | Token::Ne
1083            | Token::Lt
1084            | Token::Le
1085            | Token::Gt
1086            | Token::Ge
1087            | Token::Dot
1088            | Token::Colon
1089    )
1090}
1091
1092fn validate_update_order_by(
1093    clauses: &[OrderByClause],
1094    position: crate::lexer::Position,
1095) -> Result<(), ParseError> {
1096    for clause in clauses {
1097        if clause.expr.is_some() {
1098            return Err(ParseError::new(
1099                "UPDATE ORDER BY only supports top-level fields",
1100                position,
1101            ));
1102        }
1103        match &clause.field {
1104            FieldRef::TableColumn { table, column }
1105                if table.is_empty() && !column.contains('.') => {}
1106            _ => {
1107                return Err(ParseError::new(
1108                    "UPDATE ORDER BY only supports top-level fields",
1109                    position,
1110                ));
1111            }
1112        }
1113    }
1114    Ok(())
1115}
1116
1117fn update_order_by_mentions_rid(clauses: &[OrderByClause]) -> bool {
1118    clauses.iter().any(|clause| {
1119        matches!(
1120            &clause.field,
1121            FieldRef::TableColumn { table, column }
1122                if table.is_empty() && column.eq_ignore_ascii_case("rid")
1123        )
1124    })
1125}
1126
1127fn returning_expr_not_supported(position: crate::lexer::Position) -> ParseError {
1128    ParseError::new(
1129        "NOT_YET_SUPPORTED: RETURNING expressions are not supported yet; use RETURNING * or named columns. Track a follow-up issue for RETURNING <expr>.",
1130        position,
1131    )
1132}
1133
1134fn secret_ref_value(store: &str, collection: &str, key: &str) -> Value {
1135    let mut map = reddb_types::json::Map::new();
1136    map.insert(
1137        "type".to_string(),
1138        reddb_types::json::Value::String("secret_ref".to_string()),
1139    );
1140    map.insert(
1141        "store".to_string(),
1142        reddb_types::json::Value::String(store.to_string()),
1143    );
1144    map.insert(
1145        "collection".to_string(),
1146        reddb_types::json::Value::String(collection.to_string()),
1147    );
1148    map.insert(
1149        "key".to_string(),
1150        reddb_types::json::Value::String(key.to_string()),
1151    );
1152    Value::Json(
1153        reddb_types::json::to_vec(&reddb_types::json::Value::Object(map)).unwrap_or_default(),
1154    )
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159    use super::*;
1160    use crate::ast::{InsertEntityType, ReturningItem, UpdateTarget};
1161
1162    fn make_parser(input: &str) -> Parser<'_> {
1163        Parser::new(input).expect("lexer")
1164    }
1165
1166    fn insert(input: &str) -> InsertQuery {
1167        let mut parser = make_parser(input);
1168        let QueryExpr::Insert(query) = parser.parse_insert_query().expect("insert") else {
1169            panic!("expected insert query");
1170        };
1171        query
1172    }
1173
1174    fn update(input: &str) -> UpdateQuery {
1175        let mut parser = make_parser(input);
1176        let QueryExpr::Update(query) = parser.parse_update_query().expect("update") else {
1177            panic!("expected update query");
1178        };
1179        query
1180    }
1181
1182    fn delete(input: &str) -> DeleteQuery {
1183        let mut parser = make_parser(input);
1184        let QueryExpr::Delete(query) = parser.parse_delete_query().expect("delete") else {
1185            panic!("expected delete query");
1186        };
1187        query
1188    }
1189
1190    fn ask(input: &str) -> AskQuery {
1191        let mut parser = make_parser(input);
1192        let QueryExpr::Ask(query) = parser.parse_ask_query().expect("ask") else {
1193            panic!("expected ask query");
1194        };
1195        query
1196    }
1197
1198    #[test]
1199    fn insert_entity_types_with_options_returning_and_suppress_events() {
1200        let cases = [
1201            (
1202                "INSERT INTO items NODE (id) VALUES (1)",
1203                InsertEntityType::Node,
1204            ),
1205            (
1206                "INSERT INTO items EDGE (id) VALUES (1)",
1207                InsertEntityType::Edge,
1208            ),
1209            (
1210                "INSERT INTO items VECTOR (id) VALUES (1)",
1211                InsertEntityType::Vector,
1212            ),
1213            (
1214                "INSERT INTO items DOCUMENT VALUES ({\"id\": 1})",
1215                InsertEntityType::Document,
1216            ),
1217            ("INSERT INTO items KV (id) VALUES (1)", InsertEntityType::Kv),
1218        ];
1219        for (input, expected) in cases {
1220            assert_eq!(insert(input).entity_type, expected, "{input}");
1221        }
1222
1223        let query = insert(
1224            "INSERT INTO docs (id, body) VALUES (1, 'red'), (2, ?) \
1225             WITH TTL 2 h WITH EXPIRES AT 999 \
1226             WITH METADATA (source = 'test', score = 3) \
1227             WITH AUTO EMBED (body, title) USING openai MODEL 'text-embedding-3-small' \
1228             RETURNING * SUPPRESS EVENTS",
1229        );
1230        assert_eq!(query.table, "docs");
1231        assert_eq!(query.entity_type, InsertEntityType::Row);
1232        assert_eq!(query.columns, vec!["id", "body"]);
1233        assert_eq!(query.values.len(), 2);
1234        assert_eq!(query.value_exprs.len(), 2);
1235        assert_eq!(query.ttl_ms, Some(7_200_000));
1236        assert_eq!(query.expires_at_ms, Some(999));
1237        assert_eq!(query.with_metadata.len(), 2);
1238        assert_eq!(
1239            query.returning.as_deref(),
1240            Some([ReturningItem::All].as_slice())
1241        );
1242        let auto_embed = query.auto_embed.expect("auto embed");
1243        assert_eq!(auto_embed.fields, vec!["body", "title"]);
1244        assert_eq!(auto_embed.provider, "openai");
1245        assert_eq!(auto_embed.model.as_deref(), Some("text-embedding-3-small"));
1246        assert!(query.suppress_events);
1247    }
1248
1249    #[test]
1250    fn insert_rejects_metric_and_bad_with_clause() {
1251        let mut parser = make_parser("INSERT INTO METRIC cpu.usage (value) VALUES (1)");
1252        let err = parser
1253            .parse_insert_query()
1254            .expect_err("metric insert should fail");
1255        assert!(err.to_string().contains("INSERT INTO METRIC"));
1256
1257        let mut parser = make_parser("INSERT INTO docs (id) VALUES (1) WITH TTL 1 fortnight");
1258        let err = parser
1259            .parse_insert_query()
1260            .expect_err("bad ttl unit should fail");
1261        assert!(err.to_string().contains("unsupported TTL unit"));
1262
1263        let mut parser = make_parser("INSERT INTO docs (id) VALUES (1) WITH UNKNOWN");
1264        let err = parser
1265            .parse_insert_query()
1266            .expect_err("bad WITH should fail");
1267        assert!(err.to_string().contains("expected"));
1268    }
1269
1270    #[test]
1271    fn document_insert_canonical_bare_values_form() {
1272        // Bare inline JSON literal, no column list — the ADR 0067 form.
1273        let query = insert("INSERT INTO events DOCUMENT VALUES ({\"level\": \"info\"})");
1274        assert_eq!(query.table, "events");
1275        assert_eq!(query.entity_type, InsertEntityType::Document);
1276        assert_eq!(query.columns, vec!["body"]);
1277        assert_eq!(query.values.len(), 1);
1278        assert!(matches!(query.values[0][0], Value::Json(_)));
1279
1280        // Multi-row plus WITH clauses keep working.
1281        let query = insert(
1282            "INSERT INTO events DOCUMENT VALUES ({\"a\": 1}), ({\"b\": 2}) \
1283             WITH TTL 30 s WITH METADATA (source = 'test') RETURNING *",
1284        );
1285        assert_eq!(query.columns, vec!["body"]);
1286        assert_eq!(query.values.len(), 2);
1287        assert_eq!(query.ttl_ms, Some(30_000));
1288        assert_eq!(query.with_metadata.len(), 1);
1289        assert_eq!(
1290            query.returning.as_deref(),
1291            Some([ReturningItem::All].as_slice())
1292        );
1293    }
1294
1295    #[test]
1296    fn unmarked_bare_values_insert_parses_with_empty_columns() {
1297        // ADR 0067 (#1710): `INSERT INTO c VALUES ({…})` with no column
1298        // list and no marker parses to a Row insert with an empty column
1299        // list; the runtime infers the model from the catalog.
1300        let query = insert("INSERT INTO events VALUES ({\"level\": \"info\"})");
1301        assert_eq!(query.table, "events");
1302        assert_eq!(query.entity_type, InsertEntityType::Row);
1303        assert!(query.columns.is_empty());
1304        assert_eq!(query.values.len(), 1);
1305        assert!(matches!(query.values[0][0], Value::Json(_)));
1306
1307        // Multi-row bare VALUES keeps working.
1308        let query = insert("INSERT INTO events VALUES ({\"a\": 1}), ({\"b\": 2})");
1309        assert!(query.columns.is_empty());
1310        assert_eq!(query.entity_type, InsertEntityType::Row);
1311        assert_eq!(query.values.len(), 2);
1312
1313        // An explicit column list is still parsed positionally.
1314        let query = insert("INSERT INTO t (id) VALUES (1)");
1315        assert_eq!(query.columns, vec!["id"]);
1316    }
1317
1318    #[test]
1319    fn document_insert_rejects_removed_forms() {
1320        // `(body)` column list -> didactic bare-VALUES rewrite.
1321        let mut parser =
1322            make_parser("INSERT INTO events DOCUMENT (body) VALUES ({\"level\": \"info\"})");
1323        let err = parser
1324            .parse_insert_query()
1325            .expect_err("document column list should fail");
1326        assert!(err.to_string().contains("column list is removed"), "{err}");
1327
1328        // Legacy `_ttl` column -> point at WITH TTL.
1329        let mut parser =
1330            make_parser("INSERT INTO events DOCUMENT (body, _ttl) VALUES ({\"a\": 1}, 30)");
1331        let err = parser
1332            .parse_insert_query()
1333            .expect_err("legacy _ttl column should fail");
1334        assert!(err.to_string().contains("WITH TTL"), "{err}");
1335
1336        // Quoted-string body coercion -> inline literal + JSON_PARSE.
1337        let mut parser =
1338            make_parser("INSERT INTO events DOCUMENT VALUES ('{\"level\": \"info\"}')");
1339        let err = parser
1340            .parse_insert_query()
1341            .expect_err("quoted-string body should fail");
1342        let msg = err.to_string();
1343        assert!(msg.contains("inline JSON literal"), "{msg}");
1344        assert!(msg.contains("JSON_PARSE"), "{msg}");
1345    }
1346
1347    #[test]
1348    fn update_targets_compound_assignments_order_limit_returning() {
1349        let cases = [
1350            ("UPDATE docs KV SET count = 1", UpdateTarget::Kv),
1351            ("UPDATE docs ROWS SET count = 1", UpdateTarget::Rows),
1352            (
1353                "UPDATE docs DOCUMENTS SET count = 1",
1354                UpdateTarget::Documents,
1355            ),
1356            ("UPDATE docs NODES SET count = 1", UpdateTarget::Nodes),
1357            ("UPDATE docs EDGES SET count = 1", UpdateTarget::Edges),
1358        ];
1359        for (input, expected) in cases {
1360            assert_eq!(update(input).target, expected, "{input}");
1361        }
1362
1363        let query = update(
1364            "UPDATE docs DOCUMENTS SET count += 2, title = UPPER(title) \
1365             WHERE id = 1 WITH TTL 30 s WITH METADATA (source = 'update') \
1366             ORDER BY updated_at DESC LIMIT 5 RETURNING weight, title SUPPRESS EVENTS",
1367        );
1368        assert_eq!(query.table, "docs");
1369        assert_eq!(query.target, UpdateTarget::Documents);
1370        assert_eq!(query.assignment_exprs.len(), 2);
1371        assert_eq!(query.compound_assignment_ops, vec![Some(BinOp::Add), None]);
1372        assert_eq!(query.assignments.len(), 0);
1373        assert!(query.filter.is_some());
1374        assert!(query.where_expr.is_some());
1375        assert_eq!(query.ttl_ms, Some(30_000));
1376        assert_eq!(query.with_metadata.len(), 1);
1377        assert_eq!(query.claim_limit, None);
1378        assert!(!query.claim_exact);
1379        assert_eq!(query.limit, Some(5));
1380        assert_eq!(query.order_by.len(), 2);
1381        assert!(matches!(
1382            &query.order_by[1].field,
1383            FieldRef::TableColumn { column, .. } if column == "rid"
1384        ));
1385        assert_eq!(
1386            query.returning.as_deref(),
1387            Some(
1388                [
1389                    ReturningItem::Column("weight".to_string()),
1390                    ReturningItem::Column("title".to_string())
1391                ]
1392                .as_slice()
1393            )
1394        );
1395        assert!(query.suppress_events);
1396    }
1397
1398    #[test]
1399    fn update_claim_limit_requires_order_by() {
1400        let query = update(
1401            "UPDATE docs SET status = 'reserved' WHERE status = 'available' \
1402             CLAIM LIMIT 2 ORDER BY priority ASC RETURNING id",
1403        );
1404        assert_eq!(query.claim_limit, Some(2));
1405        assert!(!query.claim_exact);
1406        assert_eq!(query.limit, None);
1407        assert_eq!(query.order_by.len(), 2);
1408        assert!(matches!(
1409            &query.order_by[1].field,
1410            FieldRef::TableColumn { column, .. } if column == "rid"
1411        ));
1412
1413        let query = update(
1414            "UPDATE docs SET status = 'reserved' WHERE status = 'available' \
1415             CLAIM EXACT 2 ORDER BY priority ASC RETURNING id",
1416        );
1417        assert_eq!(query.claim_limit, Some(2));
1418        assert!(query.claim_exact);
1419        assert_eq!(query.limit, None);
1420        assert_eq!(query.order_by.len(), 2);
1421        assert!(matches!(
1422            &query.order_by[1].field,
1423            FieldRef::TableColumn { column, .. } if column == "rid"
1424        ));
1425
1426        let mut parser = make_parser("UPDATE docs SET status = 'reserved' CLAIM LIMIT 1");
1427        let err = parser
1428            .parse_update_query()
1429            .expect_err("claim without order should fail");
1430        assert!(err.to_string().contains("CLAIM requires ORDER BY"));
1431    }
1432
1433    #[test]
1434    fn update_rejects_invalid_assignment_and_order_by_forms() {
1435        let mut parser = make_parser("UPDATE docs SET count ^= 1");
1436        let err = parser
1437            .parse_update_query()
1438            .expect_err("unknown compound assignment should fail");
1439        assert!(err.to_string().contains("expected"));
1440
1441        let mut parser = make_parser("UPDATE docs SET count = 1 ORDER BY updated_at");
1442        let err = parser
1443            .parse_update_query()
1444            .expect_err("ORDER BY without LIMIT should fail");
1445        assert!(err.to_string().contains("requires LIMIT"));
1446
1447        let mut parser = make_parser("UPDATE docs SET count = 1 ORDER BY updated_at + 1 LIMIT 1");
1448        let err = parser
1449            .parse_update_query()
1450            .expect_err("ORDER BY expression should fail");
1451        assert!(err.to_string().contains("top-level fields"));
1452    }
1453
1454    #[test]
1455    fn delete_returning_and_suppress_events() {
1456        let query = delete("DELETE FROM docs WHERE id = 1 RETURNING id, title SUPPRESS EVENTS");
1457        assert_eq!(query.table, "docs");
1458        assert!(query.filter.is_some());
1459        assert!(query.where_expr.is_some());
1460        assert_eq!(
1461            query.returning.as_deref(),
1462            Some(
1463                [
1464                    ReturningItem::Column("id".to_string()),
1465                    ReturningItem::Column("title".to_string())
1466                ]
1467                .as_slice()
1468            )
1469        );
1470        assert!(query.suppress_events);
1471
1472        let query = delete("DELETE FROM docs RETURNING *");
1473        assert_eq!(
1474            query.returning.as_deref(),
1475            Some([ReturningItem::All].as_slice())
1476        );
1477    }
1478
1479    #[test]
1480    fn returning_rejects_expression_forms() {
1481        for input in [
1482            "DELETE FROM docs RETURNING 1",
1483            "DELETE FROM docs RETURNING UPPER(title)",
1484            "DELETE FROM docs RETURNING title || body",
1485        ] {
1486            let mut parser = make_parser(input);
1487            let err = parser
1488                .parse_delete_query()
1489                .expect_err("RETURNING expression should fail");
1490            assert!(err.to_string().contains("RETURNING expressions"));
1491        }
1492    }
1493
1494    #[test]
1495    fn ask_parses_all_optional_clauses_and_cache_modes() {
1496        let query = ask(
1497            "ASK 'what changed?' USING 'openai' MODEL 'gpt' DEPTH 3 LIMIT 4 \
1498             MIN_SCORE 0.7 COLLECTION docs TEMPERATURE 0.2 SEED 42 STRICT OFF \
1499             STREAM CACHE TTL '10m'",
1500        );
1501        assert_eq!(query.question, "what changed?");
1502        assert_eq!(query.provider.as_deref(), Some("openai"));
1503        assert_eq!(query.model.as_deref(), Some("gpt"));
1504        assert_eq!(query.depth, Some(3));
1505        assert_eq!(query.limit, Some(4));
1506        assert_eq!(query.min_score, Some(0.7));
1507        assert_eq!(query.collection.as_deref(), Some("docs"));
1508        assert_eq!(query.temperature, Some(0.2));
1509        assert_eq!(query.seed, Some(42));
1510        assert!(!query.strict);
1511        assert!(query.stream);
1512        assert_eq!(query.cache, AskCacheClause::CacheTtl("10m".to_string()));
1513
1514        let query = ask("ASK ? NOCACHE");
1515        assert_eq!(query.question, "");
1516        assert_eq!(query.question_param, Some(0));
1517        assert_eq!(query.cache, AskCacheClause::NoCache);
1518    }
1519
1520    #[test]
1521    fn explain_ask_and_ask_error_paths() {
1522        let mut parser = make_parser("EXPLAIN ASK $2 STRICT ON");
1523        let QueryExpr::Ask(query) = parser.parse_explain_ask_query().expect("explain ask") else {
1524            panic!("expected ask query");
1525        };
1526        assert!(query.explain);
1527        assert_eq!(query.question_param, Some(1));
1528        assert!(query.strict);
1529
1530        let mut parser = make_parser("EXPLAIN SELECT 1");
1531        let err = parser
1532            .parse_explain_ask_query()
1533            .expect_err("missing ASK should fail");
1534        assert!(err.to_string().contains("expected"));
1535
1536        let mut parser = make_parser("ASK 'q' STRICT MAYBE");
1537        let err = parser
1538            .parse_ask_query()
1539            .expect_err("bad strict should fail");
1540        assert!(err.to_string().contains("Expected ON or OFF"));
1541
1542        let mut parser = make_parser("ASK 'q' CACHE TTL '10m' NOCACHE");
1543        let err = parser
1544            .parse_ask_query()
1545            .expect_err("duplicate cache should fail");
1546        assert!(err.to_string().contains("cache clause"));
1547
1548        let mut parser = make_parser("ASK 'q' CACHE FOREVER '10m'");
1549        let err = parser
1550            .parse_ask_query()
1551            .expect_err("bad cache ttl keyword should fail");
1552        assert!(err.to_string().contains("Expected TTL"));
1553    }
1554
1555    #[test]
1556    fn literal_value_special_constructors_arrays_and_objects() {
1557        let mut parser = make_parser("PASSWORD('pw')");
1558        assert!(matches!(
1559            parser.parse_literal_value().expect("password"),
1560            Value::Password(secret) if secret == "@@plain@@pw"
1561        ));
1562
1563        let mut parser = make_parser("SECRET('pw')");
1564        assert!(matches!(
1565            parser.parse_literal_value().expect("secret"),
1566            Value::Secret(bytes) if bytes == b"@@plain@@pw"
1567        ));
1568
1569        let mut parser = make_parser("SECRET_REF(vault, red.vault.api_key)");
1570        let value = parser.parse_literal_value().expect("secret ref");
1571        assert!(matches!(value, Value::Json(_)));
1572
1573        // Array literals parse losslessly into `Value::Array`, preserving each
1574        // element's integer/float identity (issue #1708). Vector-vs-JSON typing
1575        // is resolved downstream from the target, not guessed at parse time.
1576        let mut parser = make_parser("[1, 2.5]");
1577        assert!(matches!(
1578            parser.parse_literal_value().expect("array"),
1579            Value::Array(items)
1580                if items == vec![Value::Integer(1), Value::Float(2.5)]
1581        ));
1582
1583        let mut parser = make_parser("['a', 2]");
1584        assert!(matches!(
1585            parser.parse_literal_value().expect("mixed array"),
1586            Value::Array(items)
1587                if items == vec![Value::Text("a".into()), Value::Integer(2)]
1588        ));
1589
1590        // A large integer that cannot survive an f32 (or even f64) round-trip
1591        // keeps its exact `Value::Integer` identity through the parser.
1592        let mut parser = make_parser("[1, 2, 9007199254740993]");
1593        assert!(matches!(
1594            parser.parse_literal_value().expect("lossless big-int array"),
1595            Value::Array(items)
1596                if items
1597                    == vec![
1598                        Value::Integer(1),
1599                        Value::Integer(2),
1600                        Value::Integer(9007199254740993),
1601                    ]
1602        ));
1603
1604        let mut parser = make_parser("{level = 'info', count: 2}");
1605        assert!(matches!(
1606            parser.parse_literal_value().expect("json object"),
1607            Value::Json(_)
1608        ));
1609    }
1610
1611    #[test]
1612    fn literal_value_rejects_invalid_secret_ref_and_scalar_start() {
1613        let mut parser = make_parser("SECRET_REF(config, red.vault.api_key)");
1614        let err = parser
1615            .parse_literal_value()
1616            .expect_err("non-vault secret ref should fail");
1617        assert!(err.to_string().contains("expected"));
1618
1619        let mut parser = make_parser("ORDER");
1620        let err = parser
1621            .parse_literal_value()
1622            .expect_err("non literal should fail");
1623        assert!(err.to_string().contains("expected"));
1624    }
1625
1626    #[test]
1627    fn json_depth_check_rejects_deep_literals() {
1628        let mut deep = reddb_types::utils::json::JsonValue::Array(vec![]);
1629        for _ in 0..JSON_LITERAL_MAX_DEPTH {
1630            deep = reddb_types::utils::json::JsonValue::Array(vec![deep]);
1631        }
1632        let err = json_literal_depth_check(&deep).expect_err("depth should fail");
1633        assert!(err.contains("JSON_LITERAL_MAX_DEPTH"));
1634    }
1635}