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