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                // Empty means "no explicit provider" — the runtime resolves
314                // it via the embeddings task pointer (ADR-0068 §5). Only an
315                // explicit `USING` names a provider here.
316                let provider = if self.consume(&Token::Using)? {
317                    self.expect_ident()?
318                } else {
319                    String::new()
320                };
321                let model = if self.consume_ident_ci("MODEL")? {
322                    Some(self.parse_string()?)
323                } else {
324                    None
325                };
326                auto_embed = Some(crate::ast::AutoEmbedConfig {
327                    fields,
328                    provider,
329                    model,
330                });
331            } else {
332                return Err(ParseError::expected(
333                    vec!["TTL", "EXPIRES AT", "METADATA", "AUTO EMBED"],
334                    self.peek(),
335                    self.position(),
336                ));
337            }
338        }
339
340        Ok((ttl_ms, expires_at_ms, with_metadata, auto_embed))
341    }
342
343    /// Expect a case-insensitive identifier (error if not found)
344    fn expect_ident_ci(&mut self, expected: &str) -> Result<(), ParseError> {
345        if self.consume_ident_ci(expected)? {
346            Ok(())
347        } else {
348            Err(ParseError::expected(
349                vec![expected],
350                self.peek(),
351                self.position(),
352            ))
353        }
354    }
355
356    /// Parse an absolute expiration timestamp (unix ms or string date)
357    fn parse_expires_at_value(&mut self) -> Result<u64, ParseError> {
358        // Try integer (unix timestamp in ms)
359        if let Ok(value) = self.parse_integer() {
360            return Ok(value as u64);
361        }
362        // Try string like '2026-12-31' — convert to unix ms
363        if let Ok(text) = self.parse_string() {
364            // Simple ISO date parsing: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS
365            let trimmed = text.trim();
366            if let Ok(ts) = trimmed.parse::<u64>() {
367                return Ok(ts);
368            }
369            // Basic date parsing — delegate to chrono if available, or simple heuristic
370            return Err(ParseError::new(
371                // F-05: `trimmed` is caller-controlled string-literal bytes.
372                // Render via `{:?}` so CR/LF/NUL/quotes are escaped before
373                // the message reaches the JSON / audit / log / gRPC sinks.
374                format!("EXPIRES AT requires a unix timestamp in milliseconds, got {trimmed:?}"),
375                self.position(),
376            ));
377        }
378        Err(ParseError::expected(
379            vec!["timestamp (unix ms) or 'YYYY-MM-DD'"],
380            self.peek(),
381            self.position(),
382        ))
383    }
384
385    /// Parse WITH METADATA (key1 = 'value1', key2 = 42)
386    fn parse_with_metadata_pairs(&mut self) -> Result<Vec<(String, Value)>, ParseError> {
387        self.expect(Token::LParen)?;
388        let mut pairs = Vec::new();
389        if !self.check(&Token::RParen) {
390            loop {
391                let key = self.expect_ident_or_keyword()?.to_ascii_lowercase();
392                self.expect(Token::Eq)?;
393                let value = self.parse_literal_value()?;
394                pairs.push((key, value));
395                if !self.consume(&Token::Comma)? {
396                    break;
397                }
398            }
399        }
400        self.expect(Token::RParen)?;
401        Ok(pairs)
402    }
403
404    /// Parse: UPDATE table SET col1=val1, col2=val2 [WHERE filter] [WITH TTL|EXPIRES AT|METADATA]
405    pub fn parse_update_query(&mut self) -> Result<QueryExpr, ParseError> {
406        self.expect(Token::Update)?;
407        let table = self.expect_ident()?;
408        let target = self.parse_update_target()?;
409        self.expect(Token::Set)?;
410
411        let mut assignments = Vec::new();
412        let mut assignment_exprs = Vec::new();
413        let mut compound_assignment_ops = Vec::new();
414        loop {
415            let col = self.parse_update_assignment_target()?;
416            let compound_op = if self.consume(&Token::Eq)? {
417                None
418            } else {
419                let op = match self.peek() {
420                    Token::Plus => BinOp::Add,
421                    Token::Dash | Token::Minus => BinOp::Sub,
422                    Token::Star => BinOp::Mul,
423                    Token::Slash => BinOp::Div,
424                    Token::Percent => BinOp::Mod,
425                    _ => {
426                        return Err(ParseError::expected(
427                            vec!["=", "+=", "-=", "*=", "/=", "%="],
428                            self.peek(),
429                            self.position(),
430                        ));
431                    }
432                };
433                self.advance()?;
434                self.expect(Token::Eq)?;
435                Some(op)
436            };
437            let expr = self.parse_expr()?;
438            let folded = fold_expr_to_value(expr.clone()).ok();
439            assignment_exprs.push((col.clone(), expr));
440            compound_assignment_ops.push(compound_op);
441            if compound_op.is_none() {
442                if let Some(val) = folded {
443                    assignments.push((col.clone(), val));
444                }
445            }
446            if !self.consume(&Token::Comma)? {
447                break;
448            }
449        }
450
451        let filter = if self.consume(&Token::Where)? {
452            Some(self.parse_filter()?)
453        } else {
454            None
455        };
456        let where_expr = filter.as_ref().map(filter_to_expr);
457
458        let (ttl_ms, expires_at_ms, with_metadata, _auto_embed) = self.parse_with_clauses()?;
459
460        let (claim_limit, claim_exact) = if self.consume_ident_ci("CLAIM")? {
461            if self.consume_ident_ci("EXACT")? {
462                (Some(self.parse_integer()? as u64), true)
463            } else {
464                self.expect(Token::Limit)?;
465                (Some(self.parse_integer()? as u64), false)
466            }
467        } else {
468            (None, false)
469        };
470
471        let mut order_by = if self.consume(&Token::Order)? {
472            self.expect(Token::By)?;
473            let clauses = self.parse_order_by_list()?;
474            validate_update_order_by(&clauses, self.position())?;
475            clauses
476        } else {
477            Vec::new()
478        };
479
480        // Optional `LIMIT N` — used by `BATCH N ROWS` data migrations
481        // to cap a single batch. Must come after WHERE / WITH because
482        // those have their own keyword tokens that the LIMIT branch
483        // would otherwise mis-consume.
484        let limit = if self.consume(&Token::Limit)? {
485            Some(self.parse_integer()? as u64)
486        } else {
487            None
488        };
489        // CLAIM LIMIT acts as the LIMIT for the purpose of ORDER BY semantics:
490        // a claim without a conventional LIMIT still has a deterministic bound.
491        let effective_limit = limit.is_some() || claim_limit.is_some();
492        if !order_by.is_empty() && !effective_limit {
493            return Err(ParseError::new(
494                "UPDATE ORDER BY requires LIMIT",
495                self.position(),
496            ));
497        }
498        if claim_limit.is_some() && order_by.is_empty() {
499            return Err(ParseError::new(
500                "UPDATE CLAIM requires ORDER BY",
501                self.position(),
502            ));
503        }
504        if !order_by.is_empty() && !update_order_by_mentions_rid(&order_by) {
505            order_by.push(OrderByClause {
506                field: FieldRef::TableColumn {
507                    table: String::new(),
508                    column: "rid".to_string(),
509                },
510                expr: None,
511                ascending: true,
512                nulls_first: false,
513            });
514        }
515
516        let returning = self.parse_returning_clause()?;
517
518        let suppress_events = if self.consume_ident_ci("SUPPRESS")? {
519            self.expect_ident_ci("EVENTS")?;
520            true
521        } else {
522            false
523        };
524
525        Ok(QueryExpr::Update(UpdateQuery {
526            table,
527            target,
528            assignment_exprs,
529            compound_assignment_ops,
530            assignments,
531            where_expr,
532            filter,
533            ttl_ms,
534            expires_at_ms,
535            with_metadata,
536            returning,
537            claim_limit,
538            claim_exact,
539            order_by,
540            limit,
541            suppress_events,
542        }))
543    }
544
545    fn parse_update_assignment_target(&mut self) -> Result<String, ParseError> {
546        // Dotted assignment targets (`SET a.b.c = …`) parse for every target
547        // (ADR 0067, #1711). Whether a nested path is legal is a model
548        // question the parser cannot answer — the analyzer resolves the
549        // collection's model from the catalog and rejects dotted targets off a
550        // document collection.
551        let mut segments = vec![self.expect_column_ident()?];
552        while self.consume(&Token::Dot)? {
553            segments.push(self.expect_column_ident()?);
554        }
555        Ok(segments.join("."))
556    }
557
558    fn parse_update_target(&mut self) -> Result<UpdateTarget, ParseError> {
559        // Model markers on UPDATE are removed (ADR 0067, #1711): the catalog
560        // already knows every existing collection's model, so `DOCUMENTS` /
561        // `ROWS` / `KV` are redundant and rejected with a didactic error that
562        // points at the unmarked form. `NODES` / `EDGES` stay — a graph
563        // collection holds both record kinds, so only the user can say which
564        // one an UPDATE targets.
565        if self.check(&Token::Kv) {
566            return Err(self.removed_update_marker_error("KV"));
567        }
568        if self.check(&Token::Rows) {
569            return Err(self.removed_update_marker_error("ROWS"));
570        }
571        if matches!(self.peek(), Token::Ident(name) if name.eq_ignore_ascii_case("DOCUMENTS")) {
572            return Err(self.removed_update_marker_error("DOCUMENTS"));
573        }
574        if self.consume_ident_ci("NODES")? {
575            return Ok(UpdateTarget::Nodes);
576        }
577        if self.consume_ident_ci("EDGES")? {
578            return Ok(UpdateTarget::Edges);
579        }
580        Ok(UpdateTarget::Rows)
581    }
582
583    /// Build the didactic error for a removed UPDATE model marker (ADR 0067,
584    /// #1711). The catalog knows the collection's model, so the marker is
585    /// redundant; the message names the unmarked form and the graph exception.
586    fn removed_update_marker_error(&self, marker: &str) -> ParseError {
587        ParseError::new(
588            format!(
589                "the `{marker}` UPDATE model marker has been removed; the catalog \
590                 already knows the collection's model — write `UPDATE <collection> \
591                 SET …` with no marker. (Graph updates still name `NODES` or `EDGES`, \
592                 since a graph collection holds both record kinds.)"
593            ),
594            self.position(),
595        )
596    }
597
598    /// Parse: DELETE FROM table [WHERE filter]
599    pub fn parse_delete_query(&mut self) -> Result<QueryExpr, ParseError> {
600        self.expect(Token::Delete)?;
601        self.expect(Token::From)?;
602        let table = self.expect_ident()?;
603
604        let filter = if self.consume(&Token::Where)? {
605            Some(self.parse_filter()?)
606        } else {
607            None
608        };
609
610        let where_expr = filter.as_ref().map(filter_to_expr);
611
612        let returning = self.parse_returning_clause()?;
613
614        let suppress_events = if self.consume_ident_ci("SUPPRESS")? {
615            self.expect_ident_ci("EVENTS")?;
616            true
617        } else {
618            false
619        };
620
621        Ok(QueryExpr::Delete(DeleteQuery {
622            table,
623            where_expr,
624            filter,
625            returning,
626            suppress_events,
627        }))
628    }
629
630    /// Parse optional `RETURNING (* | col [, col ...])` clause.
631    /// Returns `None` if no RETURNING token, errors if RETURNING is present
632    /// but not followed by `*` or a non-empty column list.
633    fn parse_returning_clause(&mut self) -> Result<Option<Vec<ReturningItem>>, ParseError> {
634        if !self.consume(&Token::Returning)? {
635            return Ok(None);
636        }
637        if self.consume(&Token::Star)? {
638            return Ok(Some(vec![ReturningItem::All]));
639        }
640        let mut items = Vec::new();
641        loop {
642            if returning_expr_start(self.peek()) {
643                return Err(returning_expr_not_supported(self.position()));
644            }
645            let col = self.expect_update_returning_column()?;
646            items.push(ReturningItem::Column(col));
647            if returning_expr_tail(self.peek()) {
648                return Err(returning_expr_not_supported(self.position()));
649            }
650            if !self.consume(&Token::Comma)? {
651                break;
652            }
653        }
654        if items.is_empty() {
655            return Err(ParseError::expected(
656                vec!["*", "column name"],
657                self.peek(),
658                self.position(),
659            ));
660        }
661        Ok(Some(items))
662    }
663
664    fn expect_update_returning_column(&mut self) -> Result<String, ParseError> {
665        if self.consume(&Token::Weight)? {
666            return Ok("weight".to_string());
667        }
668        self.expect_ident_or_keyword()
669    }
670
671    /// Parse: ASK 'question' [USING provider] [MODEL 'model'] [DEPTH n]
672    /// [LIMIT n] [MIN_SCORE x] [COLLECTION col] [PLAN]
673    pub fn parse_ask_query(&mut self) -> Result<QueryExpr, ParseError> {
674        self.parse_ask_query_with_explain(false)
675    }
676
677    /// Parse: EXPLAIN ASK 'question' ...
678    pub fn parse_explain_ask_query(&mut self) -> Result<QueryExpr, ParseError> {
679        self.advance()?; // consume EXPLAIN
680        if !matches!(self.peek(), Token::Ident(name) if name.eq_ignore_ascii_case("ASK")) {
681            return Err(ParseError::expected(
682                vec!["ASK"],
683                self.peek(),
684                self.position(),
685            ));
686        }
687        self.parse_ask_query_with_explain(true)
688    }
689
690    fn parse_ask_query_with_explain(&mut self, explain: bool) -> Result<QueryExpr, ParseError> {
691        self.advance()?; // consume ASK
692
693        let (question, question_param) = match self.peek() {
694            Token::String(_) => (self.parse_string()?, None),
695            Token::Dollar | Token::Question => {
696                let index = self.parse_param_slot("ASK question")?;
697                (String::new(), Some(index))
698            }
699            other => {
700                return Err(ParseError::expected(
701                    vec!["string", "$N", "?"],
702                    other,
703                    self.position(),
704                ));
705            }
706        };
707
708        let mut provider = None;
709        let mut model = None;
710        let mut depth = None;
711        let mut limit = None;
712        let mut min_score = None;
713        let mut collection = None;
714        let mut temperature = None;
715        let mut seed = None;
716        let mut strict = true;
717        let mut stream = false;
718        let mut cache = AskCacheClause::Default;
719        let mut plan_only = false;
720        let mut steps = None;
721
722        // Parse optional clauses in any order. Loop bound = number of
723        // clause kinds, so each can appear at most once.
724        for _ in 0..15 {
725            if self.consume(&Token::Using)? {
726                provider = Some(match &self.current.token {
727                    Token::String(_) => self.parse_string()?,
728                    _ => self.expect_ident()?,
729                });
730            } else if self.consume_ident_ci("MODEL")? {
731                model = Some(self.parse_string()?);
732            } else if self.consume(&Token::Depth)? {
733                depth = Some(self.parse_integer()? as usize);
734            } else if self.consume(&Token::Limit)? {
735                limit = Some(self.parse_integer()? as usize);
736            } else if self.consume(&Token::MinScore)? {
737                min_score = Some(self.parse_float()? as f32);
738            } else if self.consume(&Token::Collection)? {
739                collection = Some(self.expect_ident()?);
740            } else if self.consume_ident_ci("TEMPERATURE")? {
741                temperature = Some(self.parse_float()? as f32);
742            } else if self.consume_ident_ci("SEED")? {
743                seed = Some(self.parse_integer()? as u64);
744            } else if self.consume_ident_ci("STRICT")? {
745                let value = self.expect_ident_or_keyword()?;
746                if value.eq_ignore_ascii_case("ON") {
747                    strict = true;
748                } else if value.eq_ignore_ascii_case("OFF") {
749                    strict = false;
750                } else {
751                    return Err(ParseError::new(
752                        "Expected ON or OFF after STRICT",
753                        self.position(),
754                    ));
755                }
756            } else if self.consume_ident_ci("STREAM")? {
757                stream = true;
758            } else if self.consume_ident_ci("CACHE")? {
759                if !matches!(cache, AskCacheClause::Default) {
760                    return Err(ParseError::new(
761                        "ASK cache clause specified more than once",
762                        self.position(),
763                    ));
764                }
765                let ttl = self.expect_ident_or_keyword()?;
766                if !ttl.eq_ignore_ascii_case("TTL") {
767                    return Err(ParseError::new("Expected TTL after CACHE", self.position()));
768                }
769                cache = AskCacheClause::CacheTtl(self.parse_string()?);
770            } else if self.consume_ident_ci("NOCACHE")? {
771                if !matches!(cache, AskCacheClause::Default) {
772                    return Err(ParseError::new(
773                        "ASK cache clause specified more than once",
774                        self.position(),
775                    ));
776                }
777                cache = AskCacheClause::NoCache;
778            } else if self.consume(&Token::As)? {
779                // Clean break (ADR 0068, #1751): `ASK ... AS RQL` was removed.
780                // Point the caller at the replacement: `PLAN` returns the
781                // typed plan and candidate query without executing.
782                return Err(ParseError::new(
783                    "ASK ... AS RQL was removed: use `ASK '<question>' PLAN` to return the \
784                     typed plan and candidate query without executing",
785                    self.position(),
786                ));
787            } else if self.consume_ident_ci("EXECUTE")? {
788                // Clean break (ADR 0068, #1751): `ASK ... EXECUTE` was removed.
789                // Read-only candidates auto-execute by default; `PLAN`
790                // inspects the query without running it.
791                return Err(ParseError::new(
792                    "ASK ... EXECUTE was removed: read-only candidates auto-execute by default; \
793                     use `ASK '<question>' PLAN` to inspect the query without executing",
794                    self.position(),
795                ));
796            } else if self.consume_ident_ci("PLAN")? {
797                if plan_only {
798                    return Err(ParseError::new(
799                        "ASK PLAN specified more than once",
800                        self.position(),
801                    ));
802                }
803                plan_only = true;
804            } else if self.consume_ident_ci("STEPS")? {
805                if steps.is_some() {
806                    return Err(ParseError::new(
807                        "ASK STEPS specified more than once",
808                        self.position(),
809                    ));
810                }
811                let n = self.parse_integer()?;
812                if n < 1 {
813                    return Err(ParseError::new(
814                        "ASK STEPS must be a positive integer",
815                        self.position(),
816                    ));
817                }
818                steps = Some(n as usize);
819            } else {
820                break;
821            }
822        }
823
824        Ok(QueryExpr::Ask(AskQuery {
825            explain,
826            question,
827            question_param,
828            provider,
829            model,
830            depth,
831            limit,
832            min_score,
833            collection,
834            temperature,
835            seed,
836            strict,
837            stream,
838            cache,
839            plan_only,
840            steps,
841        }))
842    }
843
844    /// Parse comma-separated identifiers (accepts keywords as column names in DML context)
845    fn parse_ident_list(&mut self) -> Result<Vec<String>, ParseError> {
846        let mut idents = Vec::new();
847        loop {
848            idents.push(self.expect_ident_or_keyword()?);
849            if !self.consume(&Token::Comma)? {
850                break;
851            }
852        }
853        Ok(idents)
854    }
855
856    /// Parse comma-separated literal values for DML statements
857    fn parse_dml_value_list(&mut self) -> Result<Vec<Value>, ParseError> {
858        self.parse_dml_expr_list()?
859            .into_iter()
860            .map(fold_expr_to_value)
861            .collect::<Result<Vec<_>, _>>()
862            .map_err(|msg| ParseError::new(msg, self.position()))
863    }
864
865    fn parse_dml_expr_list(&mut self) -> Result<Vec<Expr>, ParseError> {
866        let mut values = Vec::new();
867        loop {
868            values.push(self.parse_expr()?);
869            if !self.consume(&Token::Comma)? {
870                break;
871            }
872        }
873        Ok(values)
874    }
875
876    /// Parse a single literal value (string, number, true, false, null, array)
877    pub(crate) fn parse_literal_value(&mut self) -> Result<Value, ParseError> {
878        // Depth guard: this function recurses for nested array `[…]`
879        // and object `{…}` literals (see the LBracket / LBrace arms
880        // below). Without entering the depth counter, an adversarial
881        // payload like `[[[[…(10k×)…]]]]` would overflow the Rust
882        // stack BEFORE `ParserLimits::max_depth` fires. The
883        // `JsonLiteral` token path uses `json_literal_depth_check`
884        // (iterative) — the bare `[`/`{` path needs the recursion
885        // counter explicitly.
886        self.enter_depth()?;
887        let result = self.parse_literal_value_inner();
888        self.exit_depth();
889        result
890    }
891
892    fn parse_literal_value_inner(&mut self) -> Result<Value, ParseError> {
893        // Recognize PASSWORD('plaintext') and SECRET('plaintext') as
894        // typed literal constructors. The parser stores them as
895        // sentinel-prefixed values so that the INSERT executor can
896        // apply the crypto transform (argon2id hash / AES-256-GCM
897        // encrypt) without the parser depending on auth or crypto
898        // subsystems.
899        if let Token::Ident(name) = self.peek().clone() {
900            let upper = name.to_uppercase();
901            if upper == "PASSWORD" || upper == "SECRET" {
902                self.advance()?; // consume ident
903                self.expect(Token::LParen)?;
904                let plaintext = self.parse_string()?;
905                self.expect(Token::RParen)?;
906                return Ok(match upper.as_str() {
907                    "PASSWORD" => Value::Password(format!("@@plain@@{plaintext}")),
908                    "SECRET" => Value::Secret(format!("@@plain@@{plaintext}").into_bytes()),
909                    _ => unreachable!(),
910                });
911            }
912            if upper == "SECRET_REF" {
913                self.advance()?; // consume ident
914                self.expect(Token::LParen)?;
915                let store = self.expect_ident_or_keyword()?.to_ascii_lowercase();
916                if store != "vault" {
917                    return Err(ParseError::expected(
918                        vec!["vault"],
919                        self.peek(),
920                        self.position(),
921                    ));
922                }
923                self.expect(Token::Comma)?;
924                let (collection, key) =
925                    self.parse_kv_key(reddb_types::catalog::CollectionModel::Vault)?;
926                self.expect(Token::RParen)?;
927                return Ok(secret_ref_value(&store, &collection, &key));
928            }
929        }
930
931        match self.peek().clone() {
932            Token::String(s) => {
933                let s = s.clone();
934                self.advance()?;
935                Ok(Value::text(s))
936            }
937            Token::JsonLiteral(raw) => {
938                // The lexer already validated brace balance and the
939                // 16 MiB payload ceiling. Parse the raw text into a
940                // canonical JsonValue then re-encode via `to_vec` so
941                // the on-disk bytes match the quoted form.
942                self.advance()?;
943                let json_value = reddb_types::utils::json::parse_json(&raw).map_err(|err| {
944                    ParseError::new(
945                        // F-05: render the underlying parse-error string
946                        // via `{:?}` so any user fragment serde echoed
947                        // back (unexpected character, key text, …) is
948                        // Debug-escaped before reaching the downstream
949                        // JSON / audit / log / gRPC sinks.
950                        format!("invalid JSON object literal: {:?}", err.to_string()),
951                        self.position(),
952                    )
953                })?;
954                json_literal_depth_check(&json_value)
955                    .map_err(|err| ParseError::new(err, self.position()))?;
956                let canonical = reddb_types::serde_json::Value::from(json_value);
957                let bytes = reddb_types::json::to_vec(&canonical).map_err(|err| {
958                    ParseError::new(
959                        // F-05: escape the encoder error via `{:?}` so any
960                        // user fragment it carries cannot smuggle control
961                        // bytes through downstream serialization sinks.
962                        format!("failed to encode JSON literal: {:?}", err.to_string()),
963                        self.position(),
964                    )
965                })?;
966                Ok(Value::Json(bytes))
967            }
968            Token::Integer(n) => {
969                self.advance()?;
970                Ok(Value::Integer(n))
971            }
972            Token::Float(n) => {
973                self.advance()?;
974                Ok(Value::Float(n))
975            }
976            Token::True => {
977                self.advance()?;
978                Ok(Value::Boolean(true))
979            }
980            Token::False => {
981                self.advance()?;
982                Ok(Value::Boolean(false))
983            }
984            Token::Null => {
985                self.advance()?;
986                Ok(Value::Null)
987            }
988            Token::LBracket => {
989                // Parse array literal `[val1, val2, ...]` **losslessly** into a
990                // `Value::Array`, preserving each element's integer/float/other
991                // identity (issue #1708, ADR 0067). The parser deliberately does
992                // NOT decide here whether the array is a vector or a JSON array:
993                // committing to an f32 `Value::Vector` at parse time silently
994                // corrupts large integers destined for a JSON position (e.g.
995                // `[9007199254740993]`). Instead the analyzer/runtime resolves
996                // the concrete shape from the target's type — a vector-typed
997                // position coerces `Value::Array` → `Value::Vector`, a JSON
998                // position coerces it to an exact JSON array.
999                self.advance()?; // consume '['
1000                let mut items = Vec::new();
1001                if !self.check(&Token::RBracket) {
1002                    loop {
1003                        items.push(self.parse_literal_value()?);
1004                        if !self.consume(&Token::Comma)? {
1005                            break;
1006                        }
1007                    }
1008                }
1009                self.expect(Token::RBracket)?;
1010                Ok(Value::Array(items))
1011            }
1012            Token::LBrace => {
1013                // Parse JSON object literal {key: value, ...}
1014                self.advance()?; // consume '{'
1015                let mut map = reddb_types::json::Map::new();
1016                if !self.check(&Token::RBrace) {
1017                    loop {
1018                        // Key: string or identifier. Reserved-word
1019                        // keys (`level`, `msg`, `type`, …) fall through
1020                        // to `expect_ident_or_keyword`, which returns
1021                        // the canonical UPPERCASE spelling; lowercase
1022                        // that path so the JSON object preserves the
1023                        // source casing.
1024                        let key = match self.peek().clone() {
1025                            Token::String(s) => {
1026                                self.advance()?;
1027                                s
1028                            }
1029                            Token::Ident(s) => {
1030                                self.advance()?;
1031                                s
1032                            }
1033                            _ => self.expect_ident_or_keyword()?.to_ascii_lowercase(),
1034                        };
1035                        // Separator: ':' or '='
1036                        if !self.consume(&Token::Colon)? {
1037                            self.expect(Token::Eq)?;
1038                        }
1039                        // Value: recursive
1040                        let val = self.parse_literal_value()?;
1041                        map.insert(key, literal_value_to_json(&val));
1042                        if !self.consume(&Token::Comma)? {
1043                            break;
1044                        }
1045                    }
1046                }
1047                self.expect(Token::RBrace)?;
1048                let json_val = reddb_types::json::Value::Object(map);
1049                let bytes = reddb_types::json::to_vec(&json_val).unwrap_or_default();
1050                Ok(Value::Json(bytes))
1051            }
1052            ref other => Err(ParseError::expected(
1053                vec!["string", "number", "true", "false", "null", "[", "{"],
1054                other,
1055                self.position(),
1056            )),
1057        }
1058    }
1059}
1060
1061/// ADR 0067 (#1709): the legacy metadata columns whose presence in a
1062/// document INSERT column list should steer the didactic error toward
1063/// `WITH TTL`. Mirrors `resolve_sql_ttl_metadata_key` in the server
1064/// runtime; kept local so the parser has no server dependency.
1065fn is_legacy_ttl_column(column: &str) -> bool {
1066    column.eq_ignore_ascii_case("_ttl")
1067        || column.eq_ignore_ascii_case("_ttl_ms")
1068        || column.eq_ignore_ascii_case("_expires_at")
1069}
1070
1071/// Convert a parsed literal `Value` into a `reddb_types::json::Value`.
1072///
1073/// Array literals now parse into `Value::Array` (issue #1708). When an array
1074/// appears inside a JSON object-literal position (e.g.
1075/// `{roles: ['edge', 'cache']}`) it must serialise as a JSON array rather than
1076/// collapsing to `null` the way the old catch-all arm did. Recurses so nested
1077/// arrays and objects round-trip.
1078fn literal_value_to_json(val: &Value) -> reddb_types::json::Value {
1079    match val {
1080        Value::Null => reddb_types::json::Value::Null,
1081        Value::Boolean(b) => reddb_types::json::Value::Bool(*b),
1082        Value::Integer(i) => reddb_types::json::Value::Number(*i as f64),
1083        Value::Float(f) => reddb_types::json::Value::Number(*f),
1084        Value::Text(s) => reddb_types::json::Value::String(s.to_string()),
1085        Value::Json(bytes) => {
1086            reddb_types::json::from_slice(bytes).unwrap_or(reddb_types::json::Value::Null)
1087        }
1088        Value::Array(items) => {
1089            reddb_types::json::Value::Array(items.iter().map(literal_value_to_json).collect())
1090        }
1091        _ => reddb_types::json::Value::Null,
1092    }
1093}
1094
1095fn returning_expr_start(token: &Token) -> bool {
1096    matches!(
1097        token,
1098        Token::Integer(_)
1099            | Token::Float(_)
1100            | Token::String(_)
1101            | Token::JsonLiteral(_)
1102            | Token::Null
1103            | Token::True
1104            | Token::False
1105            | Token::LParen
1106            | Token::Minus
1107            | Token::Question
1108            | Token::Dollar
1109    )
1110}
1111
1112fn returning_expr_tail(token: &Token) -> bool {
1113    matches!(
1114        token,
1115        Token::LParen
1116            | Token::Plus
1117            | Token::Minus
1118            | Token::Star
1119            | Token::Slash
1120            | Token::Percent
1121            | Token::DoublePipe
1122            | Token::Pipe
1123            | Token::Eq
1124            | Token::Ne
1125            | Token::Lt
1126            | Token::Le
1127            | Token::Gt
1128            | Token::Ge
1129            | Token::Dot
1130            | Token::Colon
1131    )
1132}
1133
1134fn validate_update_order_by(
1135    clauses: &[OrderByClause],
1136    position: crate::lexer::Position,
1137) -> Result<(), ParseError> {
1138    for clause in clauses {
1139        if clause.expr.is_some() {
1140            return Err(ParseError::new(
1141                "UPDATE ORDER BY only supports top-level fields",
1142                position,
1143            ));
1144        }
1145        match &clause.field {
1146            FieldRef::TableColumn { table, column }
1147                if table.is_empty() && !column.contains('.') => {}
1148            _ => {
1149                return Err(ParseError::new(
1150                    "UPDATE ORDER BY only supports top-level fields",
1151                    position,
1152                ));
1153            }
1154        }
1155    }
1156    Ok(())
1157}
1158
1159fn update_order_by_mentions_rid(clauses: &[OrderByClause]) -> bool {
1160    clauses.iter().any(|clause| {
1161        matches!(
1162            &clause.field,
1163            FieldRef::TableColumn { table, column }
1164                if table.is_empty() && column.eq_ignore_ascii_case("rid")
1165        )
1166    })
1167}
1168
1169fn returning_expr_not_supported(position: crate::lexer::Position) -> ParseError {
1170    ParseError::new(
1171        "NOT_YET_SUPPORTED: RETURNING expressions are not supported yet; use RETURNING * or named columns. Track a follow-up issue for RETURNING <expr>.",
1172        position,
1173    )
1174}
1175
1176fn secret_ref_value(store: &str, collection: &str, key: &str) -> Value {
1177    let mut map = reddb_types::json::Map::new();
1178    map.insert(
1179        "type".to_string(),
1180        reddb_types::json::Value::String("secret_ref".to_string()),
1181    );
1182    map.insert(
1183        "store".to_string(),
1184        reddb_types::json::Value::String(store.to_string()),
1185    );
1186    map.insert(
1187        "collection".to_string(),
1188        reddb_types::json::Value::String(collection.to_string()),
1189    );
1190    map.insert(
1191        "key".to_string(),
1192        reddb_types::json::Value::String(key.to_string()),
1193    );
1194    Value::Json(
1195        reddb_types::json::to_vec(&reddb_types::json::Value::Object(map)).unwrap_or_default(),
1196    )
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201    use super::*;
1202    use crate::ast::{InsertEntityType, ReturningItem, UpdateTarget};
1203
1204    fn make_parser(input: &str) -> Parser<'_> {
1205        Parser::new(input).expect("lexer")
1206    }
1207
1208    fn insert(input: &str) -> InsertQuery {
1209        let mut parser = make_parser(input);
1210        let QueryExpr::Insert(query) = parser.parse_insert_query().expect("insert") else {
1211            panic!("expected insert query");
1212        };
1213        query
1214    }
1215
1216    fn update(input: &str) -> UpdateQuery {
1217        let mut parser = make_parser(input);
1218        let QueryExpr::Update(query) = parser.parse_update_query().expect("update") else {
1219            panic!("expected update query");
1220        };
1221        query
1222    }
1223
1224    fn delete(input: &str) -> DeleteQuery {
1225        let mut parser = make_parser(input);
1226        let QueryExpr::Delete(query) = parser.parse_delete_query().expect("delete") else {
1227            panic!("expected delete query");
1228        };
1229        query
1230    }
1231
1232    fn ask(input: &str) -> AskQuery {
1233        let mut parser = make_parser(input);
1234        let QueryExpr::Ask(query) = parser.parse_ask_query().expect("ask") else {
1235            panic!("expected ask query");
1236        };
1237        query
1238    }
1239
1240    #[test]
1241    fn insert_entity_types_with_options_returning_and_suppress_events() {
1242        let cases = [
1243            (
1244                "INSERT INTO items NODE (id) VALUES (1)",
1245                InsertEntityType::Node,
1246            ),
1247            (
1248                "INSERT INTO items EDGE (id) VALUES (1)",
1249                InsertEntityType::Edge,
1250            ),
1251            (
1252                "INSERT INTO items VECTOR (id) VALUES (1)",
1253                InsertEntityType::Vector,
1254            ),
1255            (
1256                "INSERT INTO items DOCUMENT VALUES ({\"id\": 1})",
1257                InsertEntityType::Document,
1258            ),
1259            ("INSERT INTO items KV (id) VALUES (1)", InsertEntityType::Kv),
1260        ];
1261        for (input, expected) in cases {
1262            assert_eq!(insert(input).entity_type, expected, "{input}");
1263        }
1264
1265        let query = insert(
1266            "INSERT INTO docs (id, body) VALUES (1, 'red'), (2, ?) \
1267             WITH TTL 2 h WITH EXPIRES AT 999 \
1268             WITH METADATA (source = 'test', score = 3) \
1269             WITH AUTO EMBED (body, title) USING openai MODEL 'text-embedding-3-small' \
1270             RETURNING * SUPPRESS EVENTS",
1271        );
1272        assert_eq!(query.table, "docs");
1273        assert_eq!(query.entity_type, InsertEntityType::Row);
1274        assert_eq!(query.columns, vec!["id", "body"]);
1275        assert_eq!(query.values.len(), 2);
1276        assert_eq!(query.value_exprs.len(), 2);
1277        assert_eq!(query.ttl_ms, Some(7_200_000));
1278        assert_eq!(query.expires_at_ms, Some(999));
1279        assert_eq!(query.with_metadata.len(), 2);
1280        assert_eq!(
1281            query.returning.as_deref(),
1282            Some([ReturningItem::All].as_slice())
1283        );
1284        let auto_embed = query.auto_embed.expect("auto embed");
1285        assert_eq!(auto_embed.fields, vec!["body", "title"]);
1286        assert_eq!(auto_embed.provider, "openai");
1287        assert_eq!(auto_embed.model.as_deref(), Some("text-embedding-3-small"));
1288        assert!(query.suppress_events);
1289    }
1290
1291    #[test]
1292    fn insert_rejects_metric_and_bad_with_clause() {
1293        let mut parser = make_parser("INSERT INTO METRIC cpu.usage (value) VALUES (1)");
1294        let err = parser
1295            .parse_insert_query()
1296            .expect_err("metric insert should fail");
1297        assert!(err.to_string().contains("INSERT INTO METRIC"));
1298
1299        let mut parser = make_parser("INSERT INTO docs (id) VALUES (1) WITH TTL 1 fortnight");
1300        let err = parser
1301            .parse_insert_query()
1302            .expect_err("bad ttl unit should fail");
1303        assert!(err.to_string().contains("unsupported TTL unit"));
1304
1305        let mut parser = make_parser("INSERT INTO docs (id) VALUES (1) WITH UNKNOWN");
1306        let err = parser
1307            .parse_insert_query()
1308            .expect_err("bad WITH should fail");
1309        assert!(err.to_string().contains("expected"));
1310    }
1311
1312    #[test]
1313    fn document_insert_canonical_bare_values_form() {
1314        // Bare inline JSON literal, no column list — the ADR 0067 form.
1315        let query = insert("INSERT INTO events DOCUMENT VALUES ({\"level\": \"info\"})");
1316        assert_eq!(query.table, "events");
1317        assert_eq!(query.entity_type, InsertEntityType::Document);
1318        assert_eq!(query.columns, vec!["body"]);
1319        assert_eq!(query.values.len(), 1);
1320        assert!(matches!(query.values[0][0], Value::Json(_)));
1321
1322        // Multi-row plus WITH clauses keep working.
1323        let query = insert(
1324            "INSERT INTO events DOCUMENT VALUES ({\"a\": 1}), ({\"b\": 2}) \
1325             WITH TTL 30 s WITH METADATA (source = 'test') RETURNING *",
1326        );
1327        assert_eq!(query.columns, vec!["body"]);
1328        assert_eq!(query.values.len(), 2);
1329        assert_eq!(query.ttl_ms, Some(30_000));
1330        assert_eq!(query.with_metadata.len(), 1);
1331        assert_eq!(
1332            query.returning.as_deref(),
1333            Some([ReturningItem::All].as_slice())
1334        );
1335    }
1336
1337    #[test]
1338    fn unmarked_bare_values_insert_parses_with_empty_columns() {
1339        // ADR 0067 (#1710): `INSERT INTO c VALUES ({…})` with no column
1340        // list and no marker parses to a Row insert with an empty column
1341        // list; the runtime infers the model from the catalog.
1342        let query = insert("INSERT INTO events VALUES ({\"level\": \"info\"})");
1343        assert_eq!(query.table, "events");
1344        assert_eq!(query.entity_type, InsertEntityType::Row);
1345        assert!(query.columns.is_empty());
1346        assert_eq!(query.values.len(), 1);
1347        assert!(matches!(query.values[0][0], Value::Json(_)));
1348
1349        // Multi-row bare VALUES keeps working.
1350        let query = insert("INSERT INTO events VALUES ({\"a\": 1}), ({\"b\": 2})");
1351        assert!(query.columns.is_empty());
1352        assert_eq!(query.entity_type, InsertEntityType::Row);
1353        assert_eq!(query.values.len(), 2);
1354
1355        // An explicit column list is still parsed positionally.
1356        let query = insert("INSERT INTO t (id) VALUES (1)");
1357        assert_eq!(query.columns, vec!["id"]);
1358    }
1359
1360    #[test]
1361    fn document_insert_rejects_removed_forms() {
1362        // `(body)` column list -> didactic bare-VALUES rewrite.
1363        let mut parser =
1364            make_parser("INSERT INTO events DOCUMENT (body) VALUES ({\"level\": \"info\"})");
1365        let err = parser
1366            .parse_insert_query()
1367            .expect_err("document column list should fail");
1368        assert!(err.to_string().contains("column list is removed"), "{err}");
1369
1370        // Legacy `_ttl` column -> point at WITH TTL.
1371        let mut parser =
1372            make_parser("INSERT INTO events DOCUMENT (body, _ttl) VALUES ({\"a\": 1}, 30)");
1373        let err = parser
1374            .parse_insert_query()
1375            .expect_err("legacy _ttl column should fail");
1376        assert!(err.to_string().contains("WITH TTL"), "{err}");
1377
1378        // Quoted-string body coercion -> inline literal + JSON_PARSE.
1379        let mut parser =
1380            make_parser("INSERT INTO events DOCUMENT VALUES ('{\"level\": \"info\"}')");
1381        let err = parser
1382            .parse_insert_query()
1383            .expect_err("quoted-string body should fail");
1384        let msg = err.to_string();
1385        assert!(msg.contains("inline JSON literal"), "{msg}");
1386        assert!(msg.contains("JSON_PARSE"), "{msg}");
1387    }
1388
1389    #[test]
1390    fn update_targets_compound_assignments_order_limit_returning() {
1391        // Only NODES/EDGES survive as parse-time markers (ADR 0067, #1711);
1392        // an unmarked UPDATE parses to the default Rows target and the runtime
1393        // resolves document/KV semantics from the catalog.
1394        let cases = [
1395            ("UPDATE docs SET count = 1", UpdateTarget::Rows),
1396            ("UPDATE docs NODES SET count = 1", UpdateTarget::Nodes),
1397            ("UPDATE docs EDGES SET count = 1", UpdateTarget::Edges),
1398        ];
1399        for (input, expected) in cases {
1400            assert_eq!(update(input).target, expected, "{input}");
1401        }
1402
1403        // The removed markers are rejected with a didactic error.
1404        for marker in ["DOCUMENTS", "ROWS", "KV"] {
1405            let input = format!("UPDATE docs {marker} SET count = 1");
1406            let mut parser = make_parser(&input);
1407            let err = parser
1408                .parse_update_query()
1409                .expect_err("removed update marker should fail");
1410            let msg = err.to_string();
1411            assert!(msg.contains(marker), "{msg}");
1412            assert!(msg.contains("has been removed"), "{msg}");
1413            assert!(msg.contains("with no marker"), "{msg}");
1414        }
1415
1416        let query = update(
1417            "UPDATE docs SET count += 2, title = UPPER(title) \
1418             WHERE id = 1 WITH TTL 30 s WITH METADATA (source = 'update') \
1419             ORDER BY updated_at DESC LIMIT 5 RETURNING weight, title SUPPRESS EVENTS",
1420        );
1421        assert_eq!(query.table, "docs");
1422        assert_eq!(query.target, UpdateTarget::Rows);
1423        assert_eq!(query.assignment_exprs.len(), 2);
1424        assert_eq!(query.compound_assignment_ops, vec![Some(BinOp::Add), None]);
1425        assert_eq!(query.assignments.len(), 0);
1426        assert!(query.filter.is_some());
1427        assert!(query.where_expr.is_some());
1428        assert_eq!(query.ttl_ms, Some(30_000));
1429        assert_eq!(query.with_metadata.len(), 1);
1430        assert_eq!(query.claim_limit, None);
1431        assert!(!query.claim_exact);
1432        assert_eq!(query.limit, Some(5));
1433        assert_eq!(query.order_by.len(), 2);
1434        assert!(matches!(
1435            &query.order_by[1].field,
1436            FieldRef::TableColumn { column, .. } if column == "rid"
1437        ));
1438        assert_eq!(
1439            query.returning.as_deref(),
1440            Some(
1441                [
1442                    ReturningItem::Column("weight".to_string()),
1443                    ReturningItem::Column("title".to_string())
1444                ]
1445                .as_slice()
1446            )
1447        );
1448        assert!(query.suppress_events);
1449    }
1450
1451    #[test]
1452    fn update_dotted_targets_parse_unmarked_for_every_target() {
1453        // Dotted assignment targets parse for the unmarked (Rows) target and
1454        // for graph targets alike (ADR 0067, #1711); legality is an analyzer
1455        // concern, not a parser one.
1456        let unmarked = update("UPDATE docs SET profile.address.city = 'Lisbon' WHERE name = 'ada'");
1457        assert_eq!(unmarked.target, UpdateTarget::Rows);
1458        assert_eq!(unmarked.assignment_exprs[0].0, "profile.address.city");
1459        assert_eq!(unmarked.assignments[0].0, "profile.address.city");
1460
1461        let graph = update("UPDATE social NODES SET meta.seen_at = 1");
1462        assert_eq!(graph.target, UpdateTarget::Nodes);
1463        assert_eq!(graph.assignment_exprs[0].0, "meta.seen_at");
1464    }
1465
1466    #[test]
1467    fn update_claim_limit_requires_order_by() {
1468        let query = update(
1469            "UPDATE docs SET status = 'reserved' WHERE status = 'available' \
1470             CLAIM LIMIT 2 ORDER BY priority ASC RETURNING id",
1471        );
1472        assert_eq!(query.claim_limit, Some(2));
1473        assert!(!query.claim_exact);
1474        assert_eq!(query.limit, None);
1475        assert_eq!(query.order_by.len(), 2);
1476        assert!(matches!(
1477            &query.order_by[1].field,
1478            FieldRef::TableColumn { column, .. } if column == "rid"
1479        ));
1480
1481        let query = update(
1482            "UPDATE docs SET status = 'reserved' WHERE status = 'available' \
1483             CLAIM EXACT 2 ORDER BY priority ASC RETURNING id",
1484        );
1485        assert_eq!(query.claim_limit, Some(2));
1486        assert!(query.claim_exact);
1487        assert_eq!(query.limit, None);
1488        assert_eq!(query.order_by.len(), 2);
1489        assert!(matches!(
1490            &query.order_by[1].field,
1491            FieldRef::TableColumn { column, .. } if column == "rid"
1492        ));
1493
1494        let mut parser = make_parser("UPDATE docs SET status = 'reserved' CLAIM LIMIT 1");
1495        let err = parser
1496            .parse_update_query()
1497            .expect_err("claim without order should fail");
1498        assert!(err.to_string().contains("CLAIM requires ORDER BY"));
1499    }
1500
1501    #[test]
1502    fn update_rejects_invalid_assignment_and_order_by_forms() {
1503        let mut parser = make_parser("UPDATE docs SET count ^= 1");
1504        let err = parser
1505            .parse_update_query()
1506            .expect_err("unknown compound assignment should fail");
1507        assert!(err.to_string().contains("expected"));
1508
1509        let mut parser = make_parser("UPDATE docs SET count = 1 ORDER BY updated_at");
1510        let err = parser
1511            .parse_update_query()
1512            .expect_err("ORDER BY without LIMIT should fail");
1513        assert!(err.to_string().contains("requires LIMIT"));
1514
1515        let mut parser = make_parser("UPDATE docs SET count = 1 ORDER BY updated_at + 1 LIMIT 1");
1516        let err = parser
1517            .parse_update_query()
1518            .expect_err("ORDER BY expression should fail");
1519        assert!(err.to_string().contains("top-level fields"));
1520    }
1521
1522    #[test]
1523    fn delete_returning_and_suppress_events() {
1524        let query = delete("DELETE FROM docs WHERE id = 1 RETURNING id, title SUPPRESS EVENTS");
1525        assert_eq!(query.table, "docs");
1526        assert!(query.filter.is_some());
1527        assert!(query.where_expr.is_some());
1528        assert_eq!(
1529            query.returning.as_deref(),
1530            Some(
1531                [
1532                    ReturningItem::Column("id".to_string()),
1533                    ReturningItem::Column("title".to_string())
1534                ]
1535                .as_slice()
1536            )
1537        );
1538        assert!(query.suppress_events);
1539
1540        let query = delete("DELETE FROM docs RETURNING *");
1541        assert_eq!(
1542            query.returning.as_deref(),
1543            Some([ReturningItem::All].as_slice())
1544        );
1545    }
1546
1547    #[test]
1548    fn returning_rejects_expression_forms() {
1549        for input in [
1550            "DELETE FROM docs RETURNING 1",
1551            "DELETE FROM docs RETURNING UPPER(title)",
1552            "DELETE FROM docs RETURNING title || body",
1553        ] {
1554            let mut parser = make_parser(input);
1555            let err = parser
1556                .parse_delete_query()
1557                .expect_err("RETURNING expression should fail");
1558            assert!(err.to_string().contains("RETURNING expressions"));
1559        }
1560    }
1561
1562    #[test]
1563    fn ask_parses_all_optional_clauses_and_cache_modes() {
1564        let query = ask(
1565            "ASK 'what changed?' USING 'openai' MODEL 'gpt' DEPTH 3 LIMIT 4 \
1566             MIN_SCORE 0.7 COLLECTION docs TEMPERATURE 0.2 SEED 42 STRICT OFF \
1567             STREAM CACHE TTL '10m'",
1568        );
1569        assert_eq!(query.question, "what changed?");
1570        assert_eq!(query.provider.as_deref(), Some("openai"));
1571        assert_eq!(query.model.as_deref(), Some("gpt"));
1572        assert_eq!(query.depth, Some(3));
1573        assert_eq!(query.limit, Some(4));
1574        assert_eq!(query.min_score, Some(0.7));
1575        assert_eq!(query.collection.as_deref(), Some("docs"));
1576        assert_eq!(query.temperature, Some(0.2));
1577        assert_eq!(query.seed, Some(42));
1578        assert!(!query.strict);
1579        assert!(query.stream);
1580        assert_eq!(query.cache, AskCacheClause::CacheTtl("10m".to_string()));
1581
1582        let query = ask("ASK ? NOCACHE");
1583        assert_eq!(query.question, "");
1584        assert_eq!(query.question_param, Some(0));
1585        assert_eq!(query.cache, AskCacheClause::NoCache);
1586    }
1587
1588    #[test]
1589    fn ask_parses_steps_budget_clause() {
1590        // Absent by default — the runtime falls back to the config cap.
1591        assert_eq!(ask("ASK 'q'").steps, None);
1592
1593        // A positive STEPS N is captured verbatim (clamping to the config
1594        // cap happens later, in the planner).
1595        let query = ask("ASK 'q' STEPS 2 STRICT OFF");
1596        assert_eq!(query.steps, Some(2));
1597
1598        // STEPS composes with the other clauses in any order.
1599        let query = ask("ASK 'q' DEPTH 3 STEPS 5 LIMIT 4");
1600        assert_eq!(query.steps, Some(5));
1601        assert_eq!(query.depth, Some(3));
1602        assert_eq!(query.limit, Some(4));
1603    }
1604
1605    #[test]
1606    fn ask_steps_clause_error_paths() {
1607        let mut parser = make_parser("ASK 'q' STEPS 0");
1608        let err = parser.parse_ask_query().expect_err("STEPS 0 should fail");
1609        assert!(
1610            err.to_string()
1611                .contains("ASK STEPS must be a positive integer"),
1612            "got: {err}"
1613        );
1614
1615        let mut parser = make_parser("ASK 'q' STEPS 2 STEPS 3");
1616        let err = parser
1617            .parse_ask_query()
1618            .expect_err("duplicate STEPS should fail");
1619        assert!(
1620            err.to_string()
1621                .contains("ASK STEPS specified more than once"),
1622            "got: {err}"
1623        );
1624
1625        let mut parser = make_parser("ASK 'q' STEPS abc");
1626        let err = parser
1627            .parse_ask_query()
1628            .expect_err("non-integer STEPS should fail");
1629        assert!(
1630            err.to_string().to_ascii_lowercase().contains("integer"),
1631            "got: {err}"
1632        );
1633    }
1634
1635    #[test]
1636    fn explain_ask_and_ask_error_paths() {
1637        let mut parser = make_parser("EXPLAIN ASK $2 STRICT ON");
1638        let QueryExpr::Ask(query) = parser.parse_explain_ask_query().expect("explain ask") else {
1639            panic!("expected ask query");
1640        };
1641        assert!(query.explain);
1642        assert_eq!(query.question_param, Some(1));
1643        assert!(query.strict);
1644
1645        let mut parser = make_parser("EXPLAIN SELECT 1");
1646        let err = parser
1647            .parse_explain_ask_query()
1648            .expect_err("missing ASK should fail");
1649        assert!(err.to_string().contains("expected"));
1650
1651        let mut parser = make_parser("ASK 'q' STRICT MAYBE");
1652        let err = parser
1653            .parse_ask_query()
1654            .expect_err("bad strict should fail");
1655        assert!(err.to_string().contains("Expected ON or OFF"));
1656
1657        let mut parser = make_parser("ASK 'q' CACHE TTL '10m' NOCACHE");
1658        let err = parser
1659            .parse_ask_query()
1660            .expect_err("duplicate cache should fail");
1661        assert!(err.to_string().contains("cache clause"));
1662
1663        let mut parser = make_parser("ASK 'q' CACHE FOREVER '10m'");
1664        let err = parser
1665            .parse_ask_query()
1666            .expect_err("bad cache ttl keyword should fail");
1667        assert!(err.to_string().contains("Expected TTL"));
1668    }
1669
1670    #[test]
1671    fn literal_value_special_constructors_arrays_and_objects() {
1672        let mut parser = make_parser("PASSWORD('pw')");
1673        assert!(matches!(
1674            parser.parse_literal_value().expect("password"),
1675            Value::Password(secret) if secret == "@@plain@@pw"
1676        ));
1677
1678        let mut parser = make_parser("SECRET('pw')");
1679        assert!(matches!(
1680            parser.parse_literal_value().expect("secret"),
1681            Value::Secret(bytes) if bytes == b"@@plain@@pw"
1682        ));
1683
1684        let mut parser = make_parser("SECRET_REF(vault, red.vault.api_key)");
1685        let value = parser.parse_literal_value().expect("secret ref");
1686        assert!(matches!(value, Value::Json(_)));
1687
1688        // Array literals parse losslessly into `Value::Array`, preserving each
1689        // element's integer/float identity (issue #1708). Vector-vs-JSON typing
1690        // is resolved downstream from the target, not guessed at parse time.
1691        let mut parser = make_parser("[1, 2.5]");
1692        assert!(matches!(
1693            parser.parse_literal_value().expect("array"),
1694            Value::Array(items)
1695                if items == vec![Value::Integer(1), Value::Float(2.5)]
1696        ));
1697
1698        let mut parser = make_parser("['a', 2]");
1699        assert!(matches!(
1700            parser.parse_literal_value().expect("mixed array"),
1701            Value::Array(items)
1702                if items == vec![Value::Text("a".into()), Value::Integer(2)]
1703        ));
1704
1705        // A large integer that cannot survive an f32 (or even f64) round-trip
1706        // keeps its exact `Value::Integer` identity through the parser.
1707        let mut parser = make_parser("[1, 2, 9007199254740993]");
1708        assert!(matches!(
1709            parser.parse_literal_value().expect("lossless big-int array"),
1710            Value::Array(items)
1711                if items
1712                    == vec![
1713                        Value::Integer(1),
1714                        Value::Integer(2),
1715                        Value::Integer(9007199254740993),
1716                    ]
1717        ));
1718
1719        let mut parser = make_parser("{level = 'info', count: 2}");
1720        assert!(matches!(
1721            parser.parse_literal_value().expect("json object"),
1722            Value::Json(_)
1723        ));
1724    }
1725
1726    #[test]
1727    fn literal_value_rejects_invalid_secret_ref_and_scalar_start() {
1728        let mut parser = make_parser("SECRET_REF(config, red.vault.api_key)");
1729        let err = parser
1730            .parse_literal_value()
1731            .expect_err("non-vault secret ref should fail");
1732        assert!(err.to_string().contains("expected"));
1733
1734        let mut parser = make_parser("ORDER");
1735        let err = parser
1736            .parse_literal_value()
1737            .expect_err("non literal should fail");
1738        assert!(err.to_string().contains("expected"));
1739    }
1740
1741    #[test]
1742    fn json_depth_check_rejects_deep_literals() {
1743        let mut deep = reddb_types::utils::json::JsonValue::Array(vec![]);
1744        for _ in 0..JSON_LITERAL_MAX_DEPTH {
1745            deep = reddb_types::utils::json::JsonValue::Array(vec![deep]);
1746        }
1747        let err = json_literal_depth_check(&deep).expect_err("depth should fail");
1748        assert!(err.contains("JSON_LITERAL_MAX_DEPTH"));
1749    }
1750}