Skip to main content

nedb_engine/
nql.rs

1// SPDX-FileCopyrightText: 2026 INTERCHAINED LLC
2// SPDX-License-Identifier: BUSL-1.1
3// NEDB · © 2026 INTERCHAINED LLC × Eth-Interchained × Vex (Claude Opus 5)
4
5//! NQL (NEDB Query Language) parser and executor for v2 DAG storage.
6//!
7//! Grammar:
8//!   FROM coll
9//!     [AS OF seq]
10//!     [VALID AS OF "date"]
11//!     [WHERE <predicate>]
12//!     [SEARCH "text"]
13//!     [ORDER BY field [ASC|DESC]]
14//!     [LIMIT n]
15//!     [GROUP BY field COUNT|SUM|AVG|MIN|MAX]
16//!     [TRACE caused_by [REVERSE]]
17//!
18//! where <predicate> is a full boolean expression:
19//!
20//!   <predicate> := <or>
21//!   <or>        := <and> [OR <and>]*
22//!   <and>       := <not> [AND <not>]*
23//!   <not>       := [NOT] <primary>
24//!   <primary>   := "(" <predicate> ")" | <comparison>
25//!   <comparison>:= field ( = | != | > | < | >= | <= ) value
26//!                | field [NOT] IN "(" value [, value]* ")"
27//!                | field [NOT] BETWEEN value AND value
28//!                | field [NOT] LIKE|ILIKE "pattern"
29//!                | field IS [NOT] NULL
30//!
31//! Until 3.3.0 the predicate surface was six operators wide (= != > < >= <=)
32//! joined by an implicit AND, with no grouping, no negation and no set/range/
33//! pattern tests. Every one of those is table stakes in SQL, and their absence
34//! is the single most visible gap against a SQL engine: the queries people
35//! actually type (`status IN ('open','pending')`, `height BETWEEN 100 AND 200`,
36//! `name LIKE 'ac%'`) had to be decomposed by hand or filtered client-side.
37//!
38//! NOTE ON STRICTNESS. The old parser ended its clause loop with
39//! `_ => { self.advance(); }` — "skip unrecognised". That is the same defect
40//! class as a swallowed write error: a query containing a clause the engine
41//! does not implement did not fail, it silently returned the results of a
42//! DIFFERENT query. `FROM x WHERE a = 1 OFFSET 5` dropped both tokens and
43//! answered without the offset; a misspelled `ORDRE BY height` answered
44//! unsorted. Unknown tokens are now a parse error. This is deliberately
45//! breaking for queries that were already being silently misread — there was
46//! no correct behaviour to preserve.
47
48use std::collections::HashMap;
49use anyhow::{bail, Result};
50use serde_json::{json, Value};
51
52use crate::db::Db;
53use crate::index::OrderedValue;
54use crate::store::Node;
55
56// ── Token types ──────────────────────────────────────────────────────────────
57
58#[derive(Debug, Clone, PartialEq)]
59enum Tok {
60    /// A reserved word: (UPPERCASED for matching, RAW as the user spelled it).
61    ///
62    /// The raw spelling has to survive. Field positions accept a keyword as a
63    /// field name -- a document may legitimately have a field called `count`,
64    /// `min`, `value` or `status` -- and using the uppercased form there looks
65    /// up a key that does not exist. That made `HAVING count > 1` and
66    /// `ORDER BY count DESC` silently match nothing, because they searched the
67    /// row for "COUNT".
68    Kw(String, String),
69    Ident(String),  // field name or collection name (lowercase/mixed)
70    Str(String),    // "quoted string"
71    Num(f64),       // numeric literal
72    Op(String),     // = != > < >= <=
73    Punct(char),    // ( ) ,
74    Eof,
75}
76
77struct Lexer<'a> {
78    src:  &'a str,
79    pos:  usize,
80}
81
82impl<'a> Lexer<'a> {
83    fn new(src: &'a str) -> Self { Self { src, pos: 0 } }
84
85    fn peek_char(&self) -> Option<char> { self.src[self.pos..].chars().next() }
86
87    fn skip_ws(&mut self) {
88        while let Some(c) = self.peek_char() {
89            if c.is_whitespace() { self.pos += c.len_utf8(); } else { break; }
90        }
91    }
92
93    fn next_tok(&mut self) -> Tok {
94        self.skip_ws();
95        if self.pos >= self.src.len() { return Tok::Eof; }
96
97        let c = self.peek_char().unwrap();
98
99        // Quoted string.
100        //
101        // A backslash escapes a following double-quote (\" -> a literal " that
102        // does NOT end the string). This is purely additive: a literal quote
103        // was previously impossible to express — the first " always closed the
104        // string — so no existing query can rely on the old meaning of \" and
105        // nothing breaks. Every OTHER backslash stays literal, so raw-backslash
106        // values (e.g. a Windows path) keep matching exactly as before; a
107        // regression test pins that. (A fully C-style scheme where \\ -> \
108        // would instead change the meaning of every existing backslash query,
109        // so it is deliberately NOT done here.)
110        if c == '"' {
111            self.pos += 1;
112            let mut s = String::new();
113            while let Some(ch) = self.peek_char() {
114                if ch == '"' {
115                    break;
116                }
117                if ch == '\\' {
118                    // Look at the next char: only \" collapses to ". A trailing
119                    // backslash (nothing after it) or \x for any other x stays
120                    // a literal backslash, preserving prior behavior.
121                    let next = self.src[self.pos + 1..].chars().next();
122                    if next == Some('"') {
123                        s.push('"');
124                        self.pos += 1 + 1; // consume the backslash and the quote
125                        continue;
126                    }
127                }
128                s.push(ch);
129                self.pos += ch.len_utf8();
130            }
131            if self.peek_char() == Some('"') {
132                self.pos += 1;
133            }
134            return Tok::Str(s);
135        }
136
137        // Two-char operators
138        if self.pos + 1 < self.src.len() {
139            let two = &self.src[self.pos..self.pos+2];
140            if matches!(two, "!=" | ">=" | "<=") {
141                self.pos += 2;
142                return Tok::Op(two.to_string());
143            }
144        }
145
146        // One-char operators
147        if matches!(c, '=' | '>' | '<') {
148            self.pos += 1;
149            return Tok::Op(c.to_string());
150        }
151
152        // Punctuation: grouping for boolean predicates and IN-list separators.
153        // These previously fell through to "skip unknown char", so `(`, `)` and
154        // `,` were invisible to the parser — which is why the grammar could not
155        // express either grouping or a value list.
156        if matches!(c, '(' | ')' | ',') {
157            self.pos += 1;
158            return Tok::Punct(c);
159        }
160
161        // Number
162        if c.is_ascii_digit() || (c == '-' && self.src[self.pos+1..].starts_with(|d: char| d.is_ascii_digit())) {
163            let start = self.pos;
164            if c == '-' { self.pos += 1; }
165            while let Some(d) = self.peek_char() {
166                if d.is_ascii_digit() || d == '.' { self.pos += 1; } else { break; }
167            }
168            let n: f64 = self.src[start..self.pos].parse().unwrap_or(0.0);
169            return Tok::Num(n);
170        }
171
172        // Keyword or identifier
173        if c.is_alphabetic() || c == '_' {
174            let start = self.pos;
175            while let Some(ch) = self.peek_char() {
176                if ch.is_alphanumeric() || ch == '_' || ch == '.' || ch == ':' {
177                    self.pos += ch.len_utf8();
178                } else { break; }
179            }
180            let word = &self.src[start..self.pos];
181            let upper = word.to_uppercase();
182            let keywords = ["FROM","AS","OF","VALID","WHERE","AND","OR","ORDER","BY",
183                            "ASC","DESC","LIMIT","OFFSET","GROUP","HAVING",
184                            "COUNT","SUM","AVG","MIN","MAX",
185                            "TRACE","TRAVERSE","REVERSE","SEARCH","NOT","NULL","TRUE","FALSE",
186                            "IN","BETWEEN","LIKE","ILIKE","IS"];
187            if keywords.contains(&upper.as_str()) {
188                return Tok::Kw(upper, word.to_string());
189            }
190            return Tok::Ident(word.to_string());
191        }
192
193        // Skip unknown char
194        self.pos += c.len_utf8();
195        self.next_tok()
196    }
197
198    fn tokenize(&mut self) -> Vec<Tok> {
199        let mut toks = vec![];
200        loop {
201            let t = self.next_tok();
202            if t == Tok::Eof { break; }
203            toks.push(t);
204        }
205        toks
206    }
207}
208
209// ── AST ──────────────────────────────────────────────────────────────────────
210
211/// A boolean predicate tree.
212///
213/// The old representation was `Vec<WhereClause>` evaluated with `.all()`, which
214/// can only ever express a conjunction of comparisons. A tree is required for
215/// OR, for NOT, and for parenthesised grouping — `WHERE (a = 1 OR b = 2) AND
216/// c != 3` has no encoding as a flat list.
217#[derive(Debug, Clone)]
218pub enum Pred {
219    /// field <op> value, for op in = != > < >= <=
220    Cmp { field: String, op: String, value: Value },
221    /// field [NOT] IN (v1, v2, ...)
222    In { field: String, values: Vec<Value>, negated: bool },
223    /// field [NOT] BETWEEN low AND high — inclusive on both ends, as in SQL.
224    Between { field: String, low: Value, high: Value, negated: bool },
225    /// field [NOT] LIKE "pat" — SQL wildcards: % = any run, _ = any one char.
226    /// `ci` is set by ILIKE (case-insensitive).
227    Like { field: String, pattern: String, negated: bool, ci: bool },
228    /// field IS [NOT] NULL — true when the field is JSON null OR absent.
229    IsNull { field: String, negated: bool },
230    And(Vec<Pred>),
231    Or(Vec<Pred>),
232    Not(Box<Pred>),
233}
234
235#[derive(Debug, Clone, PartialEq)]
236pub enum GroupAgg { Count, Sum, Avg, Min, Max }
237
238impl GroupAgg {
239    fn name(&self) -> &'static str {
240        match self {
241            GroupAgg::Count => "count", GroupAgg::Sum => "sum",
242            GroupAgg::Avg   => "avg",   GroupAgg::Min => "min",
243            GroupAgg::Max   => "max",
244        }
245    }
246}
247
248/// One `ORDER BY` key. A list of these replaces the old single
249/// `Option<String>` + `bool` pair, because `ORDER BY status, fee DESC` — sort
250/// by one column then break ties with another — has no encoding as a single
251/// field plus a single direction.
252#[derive(Debug, Clone, PartialEq)]
253pub struct OrderKey {
254    pub field: String,
255    pub desc:  bool,
256}
257
258/// An aggregate, grouped or ungrouped.
259///
260/// `group_field: None` is a whole-result aggregate — `FROM t COUNT`,
261/// `FROM t SUM fee` — which returns exactly one row. That was previously
262/// inexpressible: the aggregate keywords only existed after `GROUP BY`, so
263/// "how many rows match this?" had to fetch every row and count client-side.
264#[derive(Debug, Clone)]
265pub struct Aggregate {
266    pub group_field: Option<String>,
267    pub agg:         GroupAgg,
268    /// The field to aggregate. None for COUNT, which needs no target.
269    pub agg_field:   Option<String>,
270}
271
272#[derive(Debug, Clone)]
273pub struct Query {
274    pub coll:       String,
275    pub as_of:      Option<u64>,
276    pub valid_as_of: Option<String>,
277    pub where_:     Option<Pred>,
278    pub search:     Option<String>,
279    pub order_by:   Vec<OrderKey>,
280    pub limit:      Option<usize>,
281    pub offset:     Option<usize>,
282    pub aggregate:  Option<Aggregate>,
283    /// `HAVING <predicate>` — filters the AGGREGATED rows, so it can test
284    /// `count`, `sum_fee`, or the group key itself. Distinct from WHERE, which
285    /// filters input rows before they are grouped.
286    pub having:     Option<Pred>,
287    pub trace:      Option<String>,     // edge type (usually "caused_by")
288    pub trace_rev:  bool,
289    pub traverse:   Option<String>,     // named relation for TRAVERSE rel
290}
291
292// ── Parser ────────────────────────────────────────────────────────────────────
293
294struct Parser { toks: Vec<Tok>, pos: usize }
295
296impl Parser {
297    fn new(toks: Vec<Tok>) -> Self { Self { toks, pos: 0 } }
298
299    fn peek(&self) -> &Tok { self.toks.get(self.pos).unwrap_or(&Tok::Eof) }
300    fn advance(&mut self) -> Tok { let t = self.peek().clone(); self.pos += 1; t }
301
302    fn expect_kw(&mut self, kw: &str) -> Result<()> {
303        match self.advance() {
304            Tok::Kw(k, _) if k == kw => Ok(()),
305            other => bail!("expected keyword {}, got {:?}", kw, other),
306        }
307    }
308
309    /// Parse a literal.
310    ///
311    /// Returns `Result` rather than defaulting to `Value::Null`: the old arm
312    /// `_ => Value::Null` turned a syntax error into a comparison against null,
313    /// so `WHERE height > )` quietly answered "nothing is greater than null"
314    /// instead of reporting a malformed query.
315    fn parse_value(&mut self) -> Result<Value> {
316        Ok(match self.advance() {
317            Tok::Str(s)  => Value::String(s),
318            Tok::Num(n)  => json!(n),
319            Tok::Kw(k, _) if k == "NULL"  => Value::Null,
320            Tok::Kw(k, _) if k == "TRUE"  => Value::Bool(true),
321            Tok::Kw(k, _) if k == "FALSE" => Value::Bool(false),
322            Tok::Ident(s) => Value::String(s),
323            other => bail!("expected a value (string, number, TRUE, FALSE or NULL), got {:?}", other),
324        })
325    }
326
327    fn peek_kw(&self, kw: &str) -> bool {
328        matches!(self.peek(), Tok::Kw(k, _) if k == kw)
329    }
330
331    fn eat_kw(&mut self, kw: &str) -> bool {
332        if self.peek_kw(kw) { self.advance(); true } else { false }
333    }
334
335    fn expect_punct(&mut self, c: char) -> Result<()> {
336        match self.advance() {
337            Tok::Punct(p) if p == c => Ok(()),
338            other => bail!("expected '{}', got {:?}", c, other),
339        }
340    }
341
342    fn parse_agg_kw(&mut self) -> Result<GroupAgg> {
343        Ok(match self.advance() {
344            Tok::Kw(a, _) if a == "COUNT" => GroupAgg::Count,
345            Tok::Kw(a, _) if a == "SUM"   => GroupAgg::Sum,
346            Tok::Kw(a, _) if a == "AVG"   => GroupAgg::Avg,
347            Tok::Kw(a, _) if a == "MIN"   => GroupAgg::Min,
348            Tok::Kw(a, _) if a == "MAX"   => GroupAgg::Max,
349            other => bail!("expected an aggregate (COUNT/SUM/AVG/MIN/MAX), got {:?}", other),
350        })
351    }
352
353    fn parse_field(&mut self, ctx: &str) -> Result<String> {
354        match self.advance() {
355            Tok::Ident(s) | Tok::Kw(_, s) => Ok(s),
356            other => bail!("{}: expected field name, got {:?}", ctx, other),
357        }
358    }
359
360    // ── Predicate grammar: OR binds loosest, then AND, then NOT ──────────────
361
362    fn parse_pred(&mut self) -> Result<Pred> { self.parse_or() }
363
364    fn parse_or(&mut self) -> Result<Pred> {
365        let mut terms = vec![self.parse_and()?];
366        while self.eat_kw("OR") {
367            terms.push(self.parse_and()?);
368        }
369        Ok(if terms.len() == 1 { terms.pop().unwrap() } else { Pred::Or(terms) })
370    }
371
372    fn parse_and(&mut self) -> Result<Pred> {
373        let mut terms = vec![self.parse_not()?];
374        while self.peek_kw("AND") {
375            // `BETWEEN low AND high` owns its AND — it is consumed inside
376            // parse_comparison, so any AND reaching here is a real conjunction.
377            self.advance();
378            terms.push(self.parse_not()?);
379        }
380        Ok(if terms.len() == 1 { terms.pop().unwrap() } else { Pred::And(terms) })
381    }
382
383    fn parse_not(&mut self) -> Result<Pred> {
384        if self.eat_kw("NOT") {
385            return Ok(Pred::Not(Box::new(self.parse_not()?)));
386        }
387        self.parse_primary()
388    }
389
390    fn parse_primary(&mut self) -> Result<Pred> {
391        if matches!(self.peek(), Tok::Punct('(')) {
392            self.advance();
393            let inner = self.parse_pred()?;
394            self.expect_punct(')')?;
395            return Ok(inner);
396        }
397        self.parse_comparison()
398    }
399
400    fn parse_comparison(&mut self) -> Result<Pred> {
401        let field = self.parse_field("WHERE")?;
402
403        // field IS [NOT] NULL
404        if self.eat_kw("IS") {
405            let negated = self.eat_kw("NOT");
406            if !self.eat_kw("NULL") {
407                bail!("WHERE: expected NULL after IS{}", if negated { " NOT" } else { "" });
408            }
409            return Ok(Pred::IsNull { field, negated });
410        }
411
412        // A leading NOT applies to the operator that follows: IN / BETWEEN / LIKE.
413        let negated = self.eat_kw("NOT");
414
415        if self.eat_kw("IN") {
416            self.expect_punct('(')?;
417            let mut values = vec![];
418            loop {
419                values.push(self.parse_value()?);
420                if matches!(self.peek(), Tok::Punct(',')) { self.advance(); continue; }
421                break;
422            }
423            self.expect_punct(')')?;
424            if values.is_empty() {
425                bail!("WHERE: IN () needs at least one value");
426            }
427            return Ok(Pred::In { field, values, negated });
428        }
429
430        if self.eat_kw("BETWEEN") {
431            let low = self.parse_value()?;
432            if !self.eat_kw("AND") {
433                bail!("WHERE: BETWEEN expects AND between its bounds");
434            }
435            let high = self.parse_value()?;
436            return Ok(Pred::Between { field, low, high, negated });
437        }
438
439        let ci = self.peek_kw("ILIKE");
440        if ci || self.peek_kw("LIKE") {
441            self.advance();
442            let pattern = match self.advance() {
443                Tok::Str(s) => s,
444                Tok::Ident(s) => s,
445                other => bail!("WHERE: LIKE expects a pattern string, got {:?}", other),
446            };
447            return Ok(Pred::Like { field, pattern, negated, ci });
448        }
449
450        if negated {
451            bail!("WHERE: NOT must be followed by IN, BETWEEN, LIKE or ILIKE \
452                   (use `NOT (field = value)` or `field != value` to negate a comparison)");
453        }
454
455        let op = match self.advance() {
456            Tok::Op(s) => s,
457            other => bail!("WHERE: expected operator, got {:?}", other),
458        };
459        let value = self.parse_value()?;
460        Ok(Pred::Cmp { field, op, value })
461    }
462
463    fn parse(&mut self) -> Result<Query> {
464        self.expect_kw("FROM")?;
465        let coll = match self.advance() {
466            Tok::Ident(s) | Tok::Kw(_, s) => s,
467            other => bail!("expected collection name, got {:?}", other),
468        };
469
470        let mut q = Query {
471            coll, as_of: None, valid_as_of: None,
472            where_: None, search: None,
473            order_by: vec![],
474            limit: None, offset: None,
475            aggregate: None, having: None,
476            trace: None, trace_rev: false,
477            traverse: None,
478        };
479
480        loop {
481            match self.peek() {
482                Tok::Eof => break,
483
484                Tok::Kw(k, _) if k == "AS" => {
485                    self.advance();
486                    self.expect_kw("OF")?;
487                    match self.advance() {
488                        Tok::Num(n) => q.as_of = Some(n as u64),
489                        other => bail!("AS OF expects sequence number, got {:?}", other),
490                    }
491                }
492
493                Tok::Kw(k, _) if k == "VALID" => {
494                    self.advance();
495                    self.expect_kw("AS")?;
496                    self.expect_kw("OF")?;
497                    match self.advance() {
498                        Tok::Str(s) => q.valid_as_of = Some(s),
499                        other => bail!("VALID AS OF expects date string, got {:?}", other),
500                    }
501                }
502
503                Tok::Kw(k, _) if k == "WHERE" => {
504                    self.advance();
505                    let pred = self.parse_pred()?;
506                    // Repeating WHERE is a conjunction, matching the old
507                    // behaviour where every clause was ANDed together.
508                    q.where_ = Some(match q.where_.take() {
509                        None => pred,
510                        Some(prev) => Pred::And(vec![prev, pred]),
511                    });
512                }
513
514                Tok::Kw(k, _) if k == "SEARCH" => {
515                    self.advance();
516                    match self.advance() {
517                        Tok::Str(s) => q.search = Some(s),
518                        other => bail!("SEARCH expects quoted string, got {:?}", other),
519                    }
520                }
521
522                Tok::Kw(k, _) if k == "ORDER" => {
523                    self.advance();
524                    self.expect_kw("BY")?;
525                    // Comma-separated sort keys, each with its own direction:
526                    // ORDER BY status, fee DESC
527                    loop {
528                        let field = self.parse_field("ORDER BY")?;
529                        // ASC is a real keyword now. It used to lex as an Ident
530                        // and survive only because the clause loop silently
531                        // skipped tokens it did not recognise.
532                        let desc = if self.eat_kw("DESC") {
533                            true
534                        } else {
535                            self.eat_kw("ASC");
536                            false
537                        };
538                        q.order_by.push(OrderKey { field, desc });
539                        if matches!(self.peek(), Tok::Punct(',')) { self.advance(); continue; }
540                        break;
541                    }
542                }
543
544                Tok::Kw(k, _) if k == "LIMIT" => {
545                    self.advance();
546                    match self.advance() {
547                        Tok::Num(n) if n >= 0.0 => q.limit = Some(n as usize),
548                        other => bail!("LIMIT expects a non-negative number, got {:?}", other),
549                    }
550                }
551
552                Tok::Kw(k, _) if k == "OFFSET" => {
553                    self.advance();
554                    match self.advance() {
555                        Tok::Num(n) if n >= 0.0 => q.offset = Some(n as usize),
556                        other => bail!("OFFSET expects a non-negative number, got {:?}", other),
557                    }
558                }
559
560                Tok::Kw(k, _) if k == "HAVING" => {
561                    self.advance();
562                    let pred = self.parse_pred()?;
563                    q.having = Some(match q.having.take() {
564                        None => pred,
565                        Some(prev) => Pred::And(vec![prev, pred]),
566                    });
567                }
568
569                // A bare aggregate with no GROUP BY: `FROM t COUNT`,
570                // `FROM t SUM fee`. Returns exactly one row.
571                Tok::Kw(k, _) if matches!(k.as_str(), "COUNT" | "SUM" | "AVG" | "MIN" | "MAX") => {
572                    let agg = self.parse_agg_kw()?;
573                    let agg_field = match agg {
574                        GroupAgg::Count => None,
575                        _ => Some(self.parse_field("aggregate")?),
576                    };
577                    if q.aggregate.is_some() {
578                        bail!("only one aggregate per query");
579                    }
580                    q.aggregate = Some(Aggregate { group_field: None, agg, agg_field });
581                }
582
583                Tok::Kw(k, _) if k == "GROUP" => {
584                    self.advance();
585                    self.expect_kw("BY")?;
586                    let field = match self.advance() {
587                        Tok::Ident(s) | Tok::Kw(_, s) => s,
588                        other => bail!("GROUP BY: expected field, got {:?}", other),
589                    };
590                    // The aggregate is OPTIONAL, matching the Python reference
591                    // (query.py): `GROUP BY field` on its own yields per-group
592                    // counts. Rust previously REQUIRED the keyword, so a bare
593                    // GROUP BY was a parse error here and valid there.
594                    let agg = if matches!(self.peek(),
595                        Tok::Kw(a, _) if matches!(a.as_str(), "COUNT"|"SUM"|"AVG"|"MIN"|"MAX"))
596                    {
597                        self.parse_agg_kw()?
598                    } else {
599                        GroupAgg::Count
600                    };
601                    // SUM/AVG/MIN/MAX take the field to aggregate. Without it
602                    // the executor fell back to aggregating the GROUP BY field
603                    // itself, so `GROUP BY cat MAX price` reported the max
604                    // *cat* — and since a non-numeric value coerced to 1.0,
605                    // every group answered 1. The target field was lexed and
606                    // then silently dropped by the unknown-token skip.
607                    let agg_field = match agg {
608                        GroupAgg::Count => None,
609                        _ => Some(self.parse_field("GROUP BY aggregate")?),
610                    };
611                    if q.aggregate.is_some() {
612                        bail!("only one aggregate per query");
613                    }
614                    q.aggregate = Some(Aggregate {
615                        group_field: Some(field), agg, agg_field,
616                    });
617                }
618
619                Tok::Kw(k, _) if k == "TRACE" => {
620                    self.advance();
621                    let edge = match self.advance() {
622                        Tok::Ident(s) | Tok::Kw(_, s) => s,
623                        other => bail!("TRACE: expected edge type, got {:?}", other),
624                    };
625                    q.trace = Some(edge);
626                    if let Tok::Kw(k, _) = self.peek() {
627                        if k == "REVERSE" { self.advance(); q.trace_rev = true; }
628                    }
629                }
630
631                Tok::Kw(k, _) if k == "TRAVERSE" => {
632                    self.advance();
633                    let rel = match self.advance() {
634                        Tok::Ident(s) | Tok::Kw(_, s) => s,
635                        other => bail!("TRAVERSE: expected relation name, got {:?}", other),
636                    };
637                    q.traverse = Some(rel);
638                }
639
640                // Unknown token. This used to be `self.advance()` — a silent
641                // skip that answered a different query than the one asked.
642                other => bail!(
643                    "unexpected {:?} in query. Expected one of: AS OF, VALID AS OF, \
644                     WHERE, SEARCH, ORDER BY, LIMIT, OFFSET, GROUP BY, HAVING, \
645                     COUNT, SUM, AVG, MIN, MAX, TRACE, TRAVERSE",
646                    other
647                ),
648            }
649        }
650
651        Ok(q)
652    }
653}
654
655// ── Executor ──────────────────────────────────────────────────────────────────
656
657/// Resolve a field name against a node, including the `_`-prefixed metadata
658/// fields that live on the node rather than in its data payload.
659fn field_value(node: &Node, field: &str) -> Value {
660    match field {
661        "_id"   => Value::String(node.id.clone()),
662        "_coll" => Value::String(node.coll.clone()),
663        "_hash" => Value::String(node.hash.clone()),
664        "_seq"  => json!(node.seq),
665        _ => node.data.get(field).cloned().unwrap_or(Value::Null),
666    }
667}
668
669fn cmp_op(a: &Value, op: &str, b: &Value) -> bool {
670    // An ORDERING comparison against a null/missing field is never true.
671    //
672    // OrderedValue sorts Null below every number, so `<` and `<=` used to
673    // report that a document with NO `fee` field at all satisfied
674    // `WHERE fee < 5`. `>` and `>=` excluded it — the asymmetry was the tell.
675    //
676    // The Python reference has always excluded it (query.py: `if a is None:
677    // return False`, placed deliberately AFTER the = / != arms), so this was
678    // a live divergence between the two engines as well as a wrong answer:
679    // asking for cheap jobs should not return jobs with no price.
680    //
681    // `=` and `!=` keep operating on null, exactly as Python does, so
682    // `WHERE x != 5` still matches a row where x is absent and `WHERE x = NULL`
683    // still works. `BETWEEN` is built from `>=` and `<=` and so inherits this.
684    //
685    // This also makes the scan path and the sorted-index path agree. A
686    // document whose field is absent is not in that field's index, so an index
687    // range scan could never have returned it — without this fix the two paths
688    // answered the same query differently depending on whether an index
689    // happened to exist.
690    if matches!(op, "<" | "<=" | ">" | ">=") && a.is_null() {
691        return false;
692    }
693    let a = OrderedValue::from(a);
694    let b = OrderedValue::from(b);
695    match op {
696        "="  => a == b,
697        "!=" => a != b,
698        ">"  => a >  b,
699        "<"  => a <  b,
700        ">=" => a >= b,
701        "<=" => a <= b,
702        _    => false,
703    }
704}
705
706/// Render a JSON scalar for text matching. Strings pass through unquoted so a
707/// LIKE pattern is compared against the value a user sees, not against its
708/// JSON encoding (`"abc"` with the quotes included).
709fn as_text(v: &Value) -> String {
710    match v {
711        Value::String(s) => s.clone(),
712        Value::Null => String::new(),
713        other => other.to_string(),
714    }
715}
716
717/// SQL LIKE matching: `%` matches any run of characters (including empty),
718/// `_` matches exactly one. Implemented as an iterative two-pointer scan with
719/// backtracking to the last `%`, which is linear in practice and needs no
720/// regex dependency. Operates on chars, so multi-byte values match correctly.
721fn like_match(value: &str, pattern: &str, ci: bool) -> bool {
722    let (v, p): (Vec<char>, Vec<char>) = if ci {
723        (value.to_lowercase().chars().collect(), pattern.to_lowercase().chars().collect())
724    } else {
725        (value.chars().collect(), pattern.chars().collect())
726    };
727
728    let mut vi = 0usize;
729    let mut pi = 0usize;
730    // Position to resume from if the current `%` expansion turns out too short.
731    let mut star: Option<(usize, usize)> = None;
732
733    while vi < v.len() {
734        if pi < p.len() && (p[pi] == '_' || p[pi] == v[vi]) {
735            vi += 1;
736            pi += 1;
737        } else if pi < p.len() && p[pi] == '%' {
738            star = Some((pi, vi));
739            pi += 1;
740        } else if let Some((sp, sv)) = star {
741            // Backtrack: let the `%` swallow one more character.
742            pi = sp + 1;
743            vi = sv + 1;
744            star = Some((sp, vi));
745        } else {
746            return false;
747        }
748    }
749    // Trailing `%`s can still match the empty remainder.
750    while pi < p.len() && p[pi] == '%' { pi += 1; }
751    pi == p.len()
752}
753
754/// Evaluate a predicate against anything that can resolve a field name.
755///
756/// Generic over the row source so ONE implementation serves both `WHERE`
757/// (over stored nodes) and `HAVING` (over aggregated rows, which are plain
758/// JSON objects with no node behind them). Two copies would be two chances for
759/// the operators to drift apart.
760fn eval_pred_with(get: &dyn Fn(&str) -> Value, pred: &Pred) -> bool {
761    match pred {
762        Pred::Cmp { field, op, value } => cmp_op(&get(field), op, value),
763
764        Pred::In { field, values, negated } => {
765            let fv = get(field);
766            let hit = values.iter().any(|v| cmp_op(&fv, "=", v));
767            hit != *negated
768        }
769
770        Pred::Between { field, low, high, negated } => {
771            let fv = get(field);
772            // Inclusive on both ends, as in SQL.
773            let hit = cmp_op(&fv, ">=", low) && cmp_op(&fv, "<=", high);
774            hit != *negated
775        }
776
777        Pred::Like { field, pattern, negated, ci } => {
778            let fv = get(field);
779            // A missing/null field matches no pattern, and NOT LIKE on a null
780            // field stays false — mirroring SQL's three-valued logic, where a
781            // predicate over NULL is never true in either polarity.
782            if fv.is_null() { return false; }
783            let hit = like_match(&as_text(&fv), pattern, *ci);
784            hit != *negated
785        }
786
787        Pred::IsNull { field, negated } => {
788            // Absent and explicitly-null are both NULL here: a document store
789            // has no schema, so "the field was never written" and "the field
790            // holds null" are the same observable state.
791            get(field).is_null() != *negated
792        }
793
794        Pred::And(terms) => terms.iter().all(|t| eval_pred_with(get, t)),
795        Pred::Or(terms)  => terms.iter().any(|t| eval_pred_with(get, t)),
796        Pred::Not(inner) => !eval_pred_with(get, inner),
797    }
798}
799
800fn eval_pred(node: &Node, pred: &Pred) -> bool {
801    eval_pred_with(&|f| field_value(node, f), pred)
802}
803
804/// `HAVING` evaluation, over an aggregated row.
805fn eval_pred_json(obj: &Value, pred: &Pred) -> bool {
806    eval_pred_with(&|f| obj.get(f).cloned().unwrap_or(Value::Null), pred)
807}
808
809/// Sort by a list of keys, each with its own direction. Earlier keys dominate;
810/// later ones break ties.
811fn sort_by_keys<T>(rows: &mut [T], keys: &[OrderKey], get: impl Fn(&T, &str) -> Value) {
812    rows.sort_by(|a, b| {
813        for k in keys {
814            let av = OrderedValue::from(&get(a, &k.field));
815            let bv = OrderedValue::from(&get(b, &k.field));
816            let ord = if k.desc { bv.cmp(&av) } else { av.cmp(&bv) };
817            if ord != std::cmp::Ordering::Equal {
818                return ord;
819            }
820        }
821        std::cmp::Ordering::Equal
822    });
823}
824
825/// Apply OFFSET then LIMIT, in that order.
826///
827/// SQL semantics: OFFSET skips rows of the RESULT, LIMIT caps what remains.
828/// An offset past the end yields an empty page rather than an error.
829fn paginate<T>(rows: Vec<T>, offset: Option<usize>, limit: Option<usize>) -> Vec<T> {
830    let mut it = rows;
831    if let Some(off) = offset {
832        if off >= it.len() {
833            return vec![];
834        }
835        it.drain(..off);
836    }
837    if let Some(n) = limit {
838        it.truncate(n);
839    }
840    it
841}
842
843/// Collapse rows into aggregate rows.
844///
845/// With `group_field: Some(f)` this yields one row per distinct value of `f`;
846/// with `None` it yields exactly one row aggregating the whole result set.
847///
848/// `count` is the group size, while the aggregate considers ONLY rows whose
849/// target field is numeric. That split matters and matches the Python
850/// reference, which computes `count` from the group and the aggregate from
851/// `[d[af] for d in gdocs if isinstance(d[af], (int, float))]`: a group of 5
852/// rows where 2 carry a numeric `price` reports `count: 5` and averages over
853/// 2. Folding non-numeric values in as 1.0 — the behaviour before 3.3.0 —
854/// silently corrupted every SUM and AVG.
855fn aggregate_rows(rows: &[Node], spec: &Aggregate) -> Vec<Value> {
856    // `ints` tracks whether EVERY contributing value was an integer.
857    //
858    // Aggregating exclusively in f64 was both a type divergence from the
859    // Python reference (which returns `66`, not `66.0`, for a sum of integers)
860    // and a precision bug: f64 cannot represent integers above 2^53 exactly,
861    // so a SUM over satoshi amounts or block heights silently rounded. SUM /
862    // MIN / MAX now stay in i64 when the inputs are integral. AVG is always
863    // fractional — Python's `sum(nums) / len(nums)` is true division — so it
864    // stays f64 in both engines.
865    struct Group { count: usize, nums: Vec<f64>, ints: Vec<i64>, all_int: bool }
866
867    // First-seen order, so results are stable run to run. HashMap iteration
868    // order previously made the grouped output nondeterministic.
869    let mut order: Vec<String> = vec![];
870    let mut groups: HashMap<String, Group> = HashMap::new();
871
872    // The ungrouped case is one group under a fixed key, so a single code path
873    // serves both and they cannot disagree about the aggregate itself.
874    const WHOLE: &str = "";
875
876    for node in rows {
877        // Resolved through `field_value`, not `node.data`, so the `_`-prefixed
878        // metadata fields work here too. Reading the payload directly made
879        // `SELECT MAX(_seq)` return NULL — with a 200 and a plausible-looking
880        // single-row answer — even though `SELECT _seq` listed the values and
881        // `WHERE _seq > 5` filtered on them. "What is the newest sequence?" is
882        // the question replication and time travel are built on, so a silent
883        // null there was the worst shape of wrong.
884        let key = match spec.group_field {
885            None => WHOLE.to_string(),
886            Some(ref gf) => match field_value(node, gf) {
887                Value::Null => "null".to_string(),
888                v => as_text(&v),
889            },
890        };
891        let entry = groups.entry(key.clone()).or_insert_with(|| {
892            order.push(key.clone());
893            Group { count: 0, nums: vec![], ints: vec![], all_int: true }
894        });
895        entry.count += 1;
896        if let Some(ref af) = spec.agg_field {
897            // A JSON bool is not a number here, matching Python's
898            // `isinstance(x, (int, float)) and not isinstance(x, bool)`.
899            if let Value::Number(n) = field_value(node, af) {
900                if let Some(i) = n.as_i64() {
901                    entry.ints.push(i);
902                    entry.nums.push(i as f64);
903                } else if let Some(f) = n.as_f64() {
904                    entry.all_int = false;
905                    entry.nums.push(f);
906                }
907            }
908        }
909    }
910
911    // An ungrouped aggregate over ZERO rows still returns one row — COUNT of
912    // an empty set is 0, not "no answer". A grouped aggregate over zero rows
913    // correctly returns no groups.
914    if spec.group_field.is_none() && order.is_empty() {
915        order.push(WHOLE.to_string());
916        groups.insert(WHOLE.to_string(),
917                      Group { count: 0, nums: vec![], ints: vec![], all_int: true });
918    }
919
920    order.into_iter().map(|k| {
921        let g = &groups[&k];
922        let mut obj = serde_json::Map::new();
923        if let Some(ref gf) = spec.group_field {
924            obj.insert(gf.clone(), Value::String(k.clone()));
925        }
926        obj.insert("count".to_string(), json!(g.count));
927
928        // Empty aggregate input yields null, not 0 and not +/-infinity — the
929        // old fold seeded MIN with f64::INFINITY, which serialises to null
930        // anyway but would report INFINITY through any non-JSON path.
931        let int_path = g.all_int && !g.ints.is_empty();
932        let agg_val: Value = match spec.agg {
933            GroupAgg::Count => json!(g.count),
934            _ if g.nums.is_empty() => Value::Null,
935            // checked_add: an i64 overflow falls back to f64 rather than
936            // panicking in release or wrapping to a negative sum.
937            GroupAgg::Sum if int_path => {
938                match g.ints.iter().try_fold(0i64, |a, &b| a.checked_add(b)) {
939                    Some(t) => json!(t),
940                    None => json!(g.nums.iter().sum::<f64>()),
941                }
942            }
943            GroupAgg::Min if int_path => json!(g.ints.iter().min().copied().unwrap()),
944            GroupAgg::Max if int_path => json!(g.ints.iter().max().copied().unwrap()),
945            GroupAgg::Sum => json!(g.nums.iter().sum::<f64>()),
946            // AVG is true division in both engines, so always fractional.
947            GroupAgg::Avg => json!(g.nums.iter().sum::<f64>() / g.nums.len() as f64),
948            GroupAgg::Min => json!(g.nums.iter().cloned().fold(f64::INFINITY, f64::min)),
949            GroupAgg::Max => json!(g.nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max)),
950        };
951
952        // Python-parity key: sum_price / avg_score / min_price / max_price.
953        if let Some(ref af) = spec.agg_field {
954            obj.insert(format!("{}_{}", spec.agg.name(), af), agg_val.clone());
955        }
956        // `value` is retained as an alias. It was this engine's only aggregate
957        // key before 3.3.0, so Studio and any existing caller still read it;
958        // dropping it would be a silent breakage on a client we do not
959        // control from here.
960        obj.insert("value".to_string(), agg_val);
961        Value::Object(obj)
962    }).collect()
963}
964
965/// Find an `_id = "..."` equality usable as an O(1) index lookup.
966///
967/// Only descends through AND nodes. An equality sitting under an OR does not
968/// constrain the result set — `WHERE _id = "a" OR height > 3` must still return
969/// the height matches — so treating it as a point lookup would silently drop
970/// rows. That is precisely the bug the pre-existing `where_order_limit` test
971/// guards against in the ORDER BY path, one level up.
972/// What an indexed field can be narrowed to, derived from the predicate.
973#[derive(Debug, Clone)]
974enum IndexPlan {
975    /// A bounded (or half-bounded) range walk over the sorted index.
976    Range {
977        field: String,
978        low: Option<Value>,
979        high: Option<Value>,
980        low_incl: bool,
981        high_incl: bool,
982    },
983    /// A set of point lookups — `=` or `IN (...)`.
984    Values { field: String, values: Vec<Value> },
985}
986
987impl IndexPlan {
988    fn field(&self) -> &str {
989        match self {
990            IndexPlan::Range { field, .. } => field,
991            IndexPlan::Values { field, .. } => field,
992        }
993    }
994}
995
996/// Collect every constraint an AND-reachable conjunct places on a field.
997///
998/// SAFETY PROPERTY that makes this whole path sound: the returned plan only
999/// ever needs to describe a SUPERSET of the matching rows. The full predicate
1000/// is re-evaluated on whatever candidates come back, so an imprecise plan
1001/// costs time, never correctness. That is why it is fine to ignore constraints
1002/// this planner does not understand.
1003///
1004/// Only descends through `And`. A constraint under an `Or` does not restrict
1005/// the result set — `WHERE fee > 100 OR status = "open"` must still return the
1006/// status matches — so narrowing on one arm would silently drop rows. `Not` is
1007/// likewise never entered: a negated range is not a range.
1008fn collect_index_constraints(pred: &Pred, out: &mut Vec<IndexPlan>) {
1009    match pred {
1010        Pred::And(terms) => {
1011            for t in terms {
1012                collect_index_constraints(t, out);
1013            }
1014        }
1015
1016        Pred::Cmp { field, op, value } => {
1017            // `_id` has its own O(1) path and is not in the sorted index.
1018            if field == "_id" {
1019                return;
1020            }
1021            match op.as_str() {
1022                "=" => out.push(IndexPlan::Values {
1023                    field: field.clone(),
1024                    values: vec![value.clone()],
1025                }),
1026                ">" | ">=" => out.push(IndexPlan::Range {
1027                    field: field.clone(),
1028                    low: Some(value.clone()),
1029                    high: None,
1030                    low_incl: op == ">=",
1031                    high_incl: true,
1032                }),
1033                "<" | "<=" => out.push(IndexPlan::Range {
1034                    field: field.clone(),
1035                    low: None,
1036                    high: Some(value.clone()),
1037                    low_incl: true,
1038                    high_incl: op == "<=",
1039                }),
1040                // `!=` matches almost everything; a range walk would be
1041                // slower than the scan it replaces.
1042                _ => {}
1043            }
1044        }
1045
1046        Pred::Between { field, low, high, negated: false } => {
1047            out.push(IndexPlan::Range {
1048                field: field.clone(),
1049                low: Some(low.clone()),
1050                high: Some(high.clone()),
1051                low_incl: true,   // SQL BETWEEN is inclusive on both ends
1052                high_incl: true,
1053            });
1054        }
1055
1056        Pred::In { field, values, negated: false } => {
1057            out.push(IndexPlan::Values {
1058                field: field.clone(),
1059                values: values.clone(),
1060            });
1061        }
1062
1063        // NOT IN / NOT BETWEEN / LIKE / IS NULL cannot be served by a range
1064        // walk: they either match the complement of a range, or they are not
1065        // an ordering predicate at all. IS NULL specifically can NEVER use
1066        // this index — a document whose field is absent is not in the index,
1067        // so an index scan would return the exact opposite of the answer.
1068        _ => {}
1069    }
1070}
1071
1072/// Merge same-field constraints and choose the most selective indexed plan.
1073///
1074/// `fee > 10 AND fee < 100` becomes ONE bounded walk rather than a half-open
1075/// one, and when several fields are indexed the planner asks the index how
1076/// many rows each range covers and takes the narrowest — rather than
1077/// committing to whichever field it happened to see first.
1078fn choose_index_plan(db: &Db, coll: &str, pred: &Pred) -> Option<IndexPlan> {
1079    let mut raw = vec![];
1080    collect_index_constraints(pred, &mut raw);
1081    raw.retain(|p| db.has_sorted_index(coll, p.field()));
1082    if raw.is_empty() {
1083        return None;
1084    }
1085
1086    // Merge per field.
1087    let mut merged: Vec<IndexPlan> = vec![];
1088    for plan in raw {
1089        let field = plan.field().to_string();
1090        let existing = merged.iter().position(|m| m.field() == field);
1091        match (existing, plan) {
1092            (None, p) => merged.push(p),
1093
1094            // Two ranges on the same field: intersect the bounds.
1095            (Some(i), IndexPlan::Range { low, high, low_incl, high_incl, .. }) => {
1096                if let IndexPlan::Range {
1097                    low: ref mut elow, high: ref mut ehigh,
1098                    low_incl: ref mut eli, high_incl: ref mut ehi, ..
1099                } = merged[i] {
1100                    if let Some(l) = low {
1101                        let tighter = match elow {
1102                            None => true,
1103                            Some(cur) => OrderedValue::from(&l) > OrderedValue::from(&*cur),
1104                        };
1105                        if tighter { *elow = Some(l); *eli = low_incl; }
1106                    }
1107                    if let Some(h) = high {
1108                        let tighter = match ehigh {
1109                            None => true,
1110                            Some(cur) => OrderedValue::from(&h) < OrderedValue::from(&*cur),
1111                        };
1112                        if tighter { *ehigh = Some(h); *ehi = high_incl; }
1113                    }
1114                }
1115                // A Range arriving where a Values plan already sits is
1116                // ignored: the point lookups are already at least as
1117                // selective, and the predicate re-runs regardless.
1118            }
1119
1120            // An equality/IN beats a range on the same field.
1121            (Some(i), p @ IndexPlan::Values { .. }) => {
1122                if matches!(merged[i], IndexPlan::Range { .. }) {
1123                    merged[i] = p;
1124                }
1125            }
1126        }
1127    }
1128
1129    // Pick the narrowest, measured against the index rather than guessed.
1130    // A Values plan costs one point lookup per arm, so its cardinality is
1131    // the sum of those buckets.
1132    let mut best: Option<(usize, IndexPlan)> = None;
1133    for plan in merged {
1134        let card = match &plan {
1135            IndexPlan::Range { field, low, high, low_incl, high_incl } => db
1136                .range_cardinality(coll, field, low.as_ref(), high.as_ref(),
1137                                   *low_incl, *high_incl)
1138                .unwrap_or(usize::MAX),
1139            IndexPlan::Values { field, values } => values
1140                .iter()
1141                .map(|v| db.range_cardinality(coll, field, Some(v), Some(v), true, true)
1142                          .unwrap_or(usize::MAX))
1143                .fold(0usize, |a, b| a.saturating_add(b)),
1144        };
1145        if best.as_ref().map(|(c, _)| card < *c).unwrap_or(true) {
1146            best = Some((card, plan));
1147        }
1148    }
1149    best.map(|(_, p)| p)
1150}
1151
1152fn id_point_lookup(pred: &Pred) -> Option<String> {
1153    match pred {
1154        Pred::Cmp { field, op, value } if field == "_id" && op == "=" => {
1155            if let Value::String(s) = value { Some(s.clone()) } else { None }
1156        }
1157        Pred::And(terms) => terms.iter().find_map(id_point_lookup),
1158        _ => None,
1159    }
1160}
1161
1162fn matches_valid_as_of(node: &Node, date: &str) -> bool {
1163    // A node is valid at `date` if:
1164    //   valid_from is None OR valid_from <= date
1165    //   AND (valid_to is None OR valid_to > date)
1166    let from_ok = node.valid_from.as_deref().map(|f| f <= date).unwrap_or(true);
1167    let to_ok   = node.valid_to.as_deref().map(|t| t > date).unwrap_or(true);
1168    from_ok && to_ok
1169}
1170
1171fn node_contains_text(node: &Node, text: &str) -> bool {
1172    let s = node.data.to_string().to_lowercase();
1173    s.contains(&text.to_lowercase())
1174}
1175
1176/// A node as a flat query row: data fields at the top level plus the `_`-prefixed
1177/// metadata. Public so the HTTP single-row GET returns the SAME shape a query row
1178/// has — one definition, so the two surfaces cannot drift apart.
1179pub fn node_to_json(node: &Node) -> Value {
1180    let mut obj = if let Value::Object(m) = &node.data {
1181        m.clone()
1182    } else {
1183        serde_json::Map::new()
1184    };
1185    obj.insert("_id".to_string(),   Value::String(node.id.clone()));
1186    obj.insert("_hash".to_string(), Value::String(node.hash.clone()));
1187    obj.insert("_seq".to_string(),  json!(node.seq));
1188    obj.insert("_coll".to_string(), Value::String(node.coll.clone()));
1189    if let Some(ref vf) = node.valid_from {
1190        obj.insert("_valid_from".to_string(), Value::String(vf.clone()));
1191    }
1192    if let Some(ref vt) = node.valid_to {
1193        obj.insert("_valid_to".to_string(), Value::String(vt.clone()));
1194    }
1195    if !node.caused_by.is_empty() {
1196        obj.insert("_caused_by".to_string(), Value::Array(
1197            node.caused_by.iter().map(|h| Value::String(h.clone())).collect()
1198        ));
1199    }
1200    Value::Object(obj)
1201}
1202
1203/// Execute a NQL query against the DAG database.
1204/// Parse NQL into a `Query` WITHOUT touching the database.
1205///
1206/// `execute` already does exactly this as its first step; exposing it separately
1207/// lets callers validate a query before deciding to run it. The natural-language
1208/// planner (`/v1/databases/:name/cast`) uses it to answer "is this runnable?"
1209/// without side effects — checking the text against the real grammar rather than
1210/// pattern-matching it, because the parser is the only authority on that.
1211pub fn parse(nql: &str) -> Result<Query> {
1212    let mut lexer = Lexer::new(nql);
1213    let toks = lexer.tokenize();
1214    let mut parser = Parser::new(toks);
1215    parser.parse()
1216}
1217
1218pub fn execute(db: &Db, nql: &str) -> Result<Vec<Value>> {
1219    // One parse path, shared with the public `parse()` above — so validation and
1220    // execution can never disagree about what is well-formed.
1221    let q = parse(nql)?;
1222
1223    // ── Candidate generation ──────────────────────────────────────────────────
1224
1225    // Fast path: single equality filter on _id with no AS OF.
1226    // Skip the O(n) collection scan — go straight to the id index (O(1) file read).
1227    // This turns `FROM coll WHERE _id = "x" LIMIT 1` from a full-table-scan into
1228    // a single file read, giving orders-of-magnitude speedup for point lookups.
1229    let id_eq_fast_path: Option<String> = if q.as_of.is_none() && q.trace.is_none() {
1230        q.where_.as_ref().and_then(id_point_lookup)
1231    } else { None };
1232
1233    let candidates: Vec<Node> = if let Some(ref target_id) = id_eq_fast_path {
1234        // O(1) direct id-index lookup — skip full collection scan entirely
1235        db.get(&q.coll, target_id).into_iter().collect()
1236    } else if let Some(seq_target) = q.as_of {
1237        // AS OF: return each doc's version at or before target seq.
1238        //
1239        // Over the live ids PLUS the deleted ones. Walking only `id_index`
1240        // meant a DELETED document was invisible at every sequence, including
1241        // sequences before the delete where it demonstrably existed — so
1242        // `AS OF` contradicted the promise that a delete is a tombstone rather
1243        // than an erasure. `get_as_of` reaches the chain through the graveyard
1244        // pointer, and returns nothing for a sequence at or after the
1245        // tombstone, where the document really is gone.
1246        db.list_ids_including_deleted(&q.coll).into_iter()
1247            .filter_map(|id| db.get_as_of(&q.coll, &id, seq_target))
1248            .collect()
1249    } else if let Some(plan) = q.where_.as_ref()
1250        // An indexed range or point-set scan, when a sorted index covers a
1251        // field the predicate constrains.
1252        //
1253        // Deliberately NOT attempted for AS OF: the sorted index holds current
1254        // versions only (a superseded hash is dropped on overwrite), so an
1255        // index scan would answer a historical query with present-day rows.
1256        // AS OF is handled by the branch above, which walks the id index and
1257        // resolves each document at the target seq.
1258        .filter(|_| q.as_of.is_none())
1259        .and_then(|p| choose_index_plan(db, &q.coll, p))
1260    {
1261        // The full predicate re-runs on these candidates below, so the plan
1262        // only has to be a superset — it can never make the answer wrong.
1263        let got = match &plan {
1264            IndexPlan::Range { field, low, high, low_incl, high_incl } => db.range_scan(
1265                &q.coll, field, low.as_ref(), high.as_ref(), *low_incl, *high_incl),
1266            IndexPlan::Values { field, values } => db.index_lookup(&q.coll, field, values),
1267        };
1268        match got {
1269            Some(nodes) => nodes,
1270            // The index vanished between planning and execution. Fall back
1271            // rather than answering from nothing.
1272            None => db.list(&q.coll),
1273        }
1274    } else if q.order_by.len() == 1 && q.aggregate.is_none() {
1275        // ORDER BY with optional sorted index — get candidates in order.
1276        //
1277        // Single key only: the sorted index is per-field, so a multi-key sort
1278        // cannot be served from it and falls through to the post-filter sort
1279        // below. Never used when aggregating either, because the sort then
1280        // applies to the GROUPED rows, which do not exist yet.
1281        //
1282        // Push LIMIT down into the index scan ONLY when nothing filters rows
1283        // after candidate generation. WHERE / SEARCH / VALID AS OF all run on
1284        // the candidate set below, so truncating to the top-k FIRST returns
1285        // incomplete results: `WHERE n_tx > 100 ORDER BY height LIMIT 10`
1286        // would fetch the 10 lowest blocks by height and then filter — losing
1287        // matches past the top-k window. The Python reference filters → sorts
1288        // → limits (engine.py execute()); this keeps the engines in agreement.
1289        let key = &q.order_by[0];
1290        let has_post_filters = q.where_.is_some()
1291            || q.search.is_some()
1292            || q.valid_as_of.is_some();
1293        let limit = if has_post_filters {
1294            9_999_999
1295        } else {
1296            // OFFSET is applied AFTER the sort, so the pushdown has to fetch
1297            // offset + limit rows and discard the prefix later. Fetching only
1298            // `limit` would return the first page for every page.
1299            match q.limit {
1300                Some(n) => n.saturating_add(q.offset.unwrap_or(0)),
1301                None => 9_999_999,
1302            }
1303        };
1304        if key.desc {
1305            db.order_by_desc(&q.coll, &key.field, limit)
1306        } else {
1307            db.order_by_asc(&q.coll, &key.field, limit)
1308        }
1309    } else if let (Some(n), true) = (q.limit, q.where_.is_none()
1310            && q.search.is_none() && q.trace.is_none()
1311            && q.traverse.is_none() && q.aggregate.is_none()
1312            && q.order_by.is_empty() && q.offset.is_none()
1313            && q.valid_as_of.is_none()) {
1314        // LIMIT-only fast path: no filters, no ordering, no trace.
1315        // Take only the first N IDs from the id-index and fetch those docs.
1316        // This makes `FROM coll LIMIT 1` O(N) not O(total) — critical for
1317        // the Studio "Preparing…" phase which samples every collection.
1318        db.id_index
1319            .list_ids(&q.coll)
1320            .into_iter()
1321            .take(n)
1322            .filter_map(|id| db.get(&q.coll, &id))
1323            .collect()
1324    } else {
1325        // Default: all docs in collection
1326        db.list(&q.coll)
1327    };
1328
1329    // ── WHERE filter ──────────────────────────────────────────────────────────
1330
1331    let mut rows: Vec<Node> = candidates.into_iter()
1332        .filter(|n| q.where_.as_ref().map(|p| eval_pred(n, p)).unwrap_or(true))
1333        .filter(|n| q.valid_as_of.as_deref()
1334                       .map(|d| matches_valid_as_of(n, d))
1335                       .unwrap_or(true))
1336        .filter(|n| q.search.as_deref()
1337                       .map(|t| node_contains_text(n, t))
1338                       .unwrap_or(true))
1339        .collect();
1340
1341    // ── TRACE ─────────────────────────────────────────────────────────────────
1342
1343    if let Some(ref _edge_type) = q.trace {
1344        let limit = q.limit.unwrap_or(1000);
1345        let mut traced: Vec<Node> = vec![];
1346        for root in &rows {
1347            let chain = db.trace(&root.hash, q.trace_rev, limit);
1348            traced.extend(chain);
1349        }
1350        rows = traced;
1351    }
1352
1353    // ── TRAVERSE rel — one-hop named-relation lookup ──────────────────────────
1354
1355    if let Some(ref rel) = q.traverse {
1356        let mut traversed: Vec<Node> = vec![];
1357        for root in &rows {
1358            let frm = format!("{}:{}", root.coll, root.id);
1359            let neighbors = db.neighbors(&frm, rel);
1360            traversed.extend(neighbors);
1361        }
1362        rows = traversed;
1363    }
1364
1365    // ── Aggregate → HAVING → ORDER BY → OFFSET → LIMIT ───────────────────────
1366    //
1367    // This is the SQL pipeline order, and getting it wrong was a live source
1368    // of silently-wrong answers. The old order was ORDER BY → LIMIT → GROUP BY,
1369    // which means:
1370    //
1371    //   `LIMIT 5 GROUP BY status COUNT` truncated the INPUT to five rows and
1372    //   then grouped them, so with twelve rows across three statuses the
1373    //   counts summed to 5 instead of 12. A confident wrong aggregate.
1374    //
1375    //   `ORDER BY count DESC GROUP BY status COUNT` sorted the raw documents
1376    //   on a field none of them carry (`count` only exists after grouping),
1377    //   so the grouped output came back in arbitrary order and the clause was
1378    //   silently inert.
1379    //
1380    // In SQL, LIMIT and ORDER BY apply to the RESULT. They now do here.
1381
1382    if let Some(ref spec) = q.aggregate {
1383        let mut out = aggregate_rows(&rows, spec);
1384
1385        // HAVING filters the aggregated rows, so it can test `count`,
1386        // `sum_fee` or the group key — none of which exist before this point.
1387        if let Some(ref pred) = q.having {
1388            out.retain(|row| eval_pred_json(row, pred));
1389        }
1390
1391        if !q.order_by.is_empty() {
1392            sort_by_keys(&mut out, &q.order_by,
1393                         |row, f| row.get(f).cloned().unwrap_or(Value::Null));
1394        } else if let Some(ref gf) = spec.group_field {
1395            // Deterministic default: groups sorted by key.
1396            //
1397            // NOT first-seen order. The Python reference draws candidates from
1398            // a `set`, so its input row order is arbitrary; a first-seen
1399            // ordering would differ between the two engines even though both
1400            // are internally consistent. Sorting by the key gives one answer
1401            // they can agree on, which the cross-engine parity suite pins.
1402            let gf = gf.clone();
1403            out.sort_by(|a, b| {
1404                as_text(&a.get(&gf).cloned().unwrap_or(Value::Null))
1405                    .cmp(&as_text(&b.get(&gf).cloned().unwrap_or(Value::Null)))
1406            });
1407        }
1408
1409        return Ok(paginate(out, q.offset, q.limit));
1410    }
1411
1412    if q.having.is_some() {
1413        bail!("HAVING requires an aggregate — add GROUP BY <field>, or use WHERE \
1414               to filter individual rows");
1415    }
1416
1417    // ── ORDER BY (post-filter sort if no sorted index was used) ──────────────
1418
1419    if !q.order_by.is_empty() {
1420        // The single-key sorted-index path above already returned candidates
1421        // in order, but only when nothing filtered them afterwards. Re-sort
1422        // whenever a filter ran, or whenever the sort has more than one key.
1423        let index_path_held = q.order_by.len() == 1
1424            && q.as_of.is_none()
1425            && q.where_.is_none()
1426            && q.search.is_none()
1427            && q.valid_as_of.is_none()
1428            && q.trace.is_none()
1429            && q.traverse.is_none();
1430        if !index_path_held {
1431            sort_by_keys(&mut rows, &q.order_by,
1432                         |n, f| field_value(n, f));
1433        }
1434    }
1435
1436    // ── OFFSET then LIMIT ────────────────────────────────────────────────────
1437
1438    let rows = paginate(rows, q.offset, q.limit);
1439
1440    // ── Serialize ─────────────────────────────────────────────────────────────
1441
1442    Ok(rows.into_iter().map(|n| node_to_json(&n)).collect())
1443}
1444
1445/// Parse and execute NQL, returning (rows, count).
1446pub fn query(db: &Db, nql: &str) -> Result<(Vec<Value>, usize)> {
1447    let rows = execute(db, nql)?;
1448    let count = rows.len();
1449    Ok((rows, count))
1450}
1451
1452#[cfg(test)]
1453mod tests {
1454    use super::*;
1455    use tempfile::tempdir;
1456    use crate::db::Db;
1457
1458    // Returns (TempDir, Db) — the TempDir guard MUST be kept alive by the caller
1459    // (`let (_tmp, db) = setup();`). If it dropped here, its Drop would delete the
1460    // database directory out from under the live Db, and every objects.read()
1461    // (loose object files live on disk) would fail → queries return 0 rows.
1462    fn setup() -> (tempfile::TempDir, Db) {
1463        let dir = tempdir().unwrap();
1464        let db = Db::open(dir.path(), None).unwrap();
1465        db.create_sorted_index("blocks", "height");
1466        for h in 1u64..=5 {
1467            db.put("blocks", &h.to_string(),
1468                serde_json::json!({"height": h, "hash": format!("000{}", h), "n_tx": h * 2}),
1469                vec![], None, None).unwrap();
1470        }
1471        (dir, db)
1472    }
1473
1474    #[test]
1475    fn from_all() {
1476        let (_tmp, db) = setup();
1477        let (rows, count) = query(&db, "FROM blocks").unwrap();
1478        assert_eq!(count, 5);
1479        let _ = rows;
1480    }
1481
1482    #[test]
1483    fn where_eq() {
1484        let (_tmp, db) = setup();
1485        let (rows, count) = query(&db, r#"FROM blocks WHERE _id = "3""#).unwrap();
1486        assert_eq!(count, 1);
1487        assert_eq!(rows[0]["_id"], "3");
1488    }
1489
1490    #[test]
1491    fn order_by_limit() {
1492        let (_tmp, db) = setup();
1493        let (rows, count) = query(&db, "FROM blocks ORDER BY height ASC LIMIT 3").unwrap();
1494        assert_eq!(count, 3);
1495        assert_eq!(rows[0]["height"], 1);
1496        assert_eq!(rows[2]["height"], 3);
1497    }
1498
1499    #[test]
1500    fn order_by_desc() {
1501        let (_tmp, db) = setup();
1502        let (rows, _) = query(&db, "FROM blocks ORDER BY height DESC LIMIT 2").unwrap();
1503        assert_eq!(rows[0]["height"], 5);
1504    }
1505
1506    #[test]
1507    fn where_gt() {
1508        let (_tmp, db) = setup();
1509        let (rows, _) = query(&db, "FROM blocks WHERE height > 3").unwrap();
1510        assert_eq!(rows.len(), 2);
1511    }
1512
1513    /// Regression: WHERE + ORDER BY + LIMIT must not truncate candidates
1514    /// before the filter runs. setup() gives heights 1..=5 with n_tx = h*2;
1515    /// the predicate matches ONLY the two highest heights (4, 5). The old
1516    /// code passed LIMIT into the sorted-index top-k first: it fetched
1517    /// heights [1, 2], filtered on n_tx >= 8, and returned ZERO rows even
1518    /// though two matches exist. Python reference returns [4, 5].
1519    #[test]
1520    fn where_order_limit_does_not_truncate_before_filter() {
1521        let (_tmp, db) = setup();
1522        let (rows, count) =
1523            query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height LIMIT 2").unwrap();
1524        assert_eq!(count, 2, "both matching rows must survive the limit");
1525        let heights: Vec<u64> = rows.iter()
1526            .filter_map(|r| r["height"].as_u64())
1527            .collect();
1528        assert_eq!(heights, vec![4, 5]);
1529        // And the same shape DESC — top match first.
1530        let (rows_d, _) =
1531            query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height DESC LIMIT 1").unwrap();
1532        assert_eq!(rows_d.len(), 1);
1533        assert_eq!(rows_d[0]["height"], 5);
1534    }
1535
1536    // ── Predicate parity (3.3.0) ─────────────────────────────────────────────
1537    //
1538    // setup() gives blocks 1..=5 with height = h, hash = "000{h}",
1539    // n_tx = h * 2. Every test below asserts against that fixture.
1540
1541    /// A second fixture with string fields and a sparse column, for LIKE and
1542    /// IS NULL. `miner` is absent on one row on purpose.
1543    fn setup_text() -> (tempfile::TempDir, Db) {
1544        let dir = tempdir().unwrap();
1545        let db = Db::open(dir.path(), None).unwrap();
1546        let rows = [
1547            ("1", serde_json::json!({"status": "open",    "miner": "Acme Pool", "fee": 10})),
1548            ("2", serde_json::json!({"status": "pending", "miner": "acme solo", "fee": 20})),
1549            ("3", serde_json::json!({"status": "closed",  "miner": "Zenith",    "fee": 30})),
1550            ("4", serde_json::json!({"status": "open",    "fee": 40})),
1551            ("5", serde_json::json!({"status": "voided",  "miner": Value::Null, "fee": 50})),
1552        ];
1553        for (id, data) in rows {
1554            db.put("jobs", id, data, vec![], None, None).unwrap();
1555        }
1556        (dir, db)
1557    }
1558
1559    fn ids(rows: &[Value]) -> Vec<String> {
1560        let mut v: Vec<String> = rows.iter()
1561            .filter_map(|r| r["_id"].as_str().map(String::from))
1562            .collect();
1563        v.sort();
1564        v
1565    }
1566
1567    #[test]
1568    fn where_in_list() {
1569        let (_tmp, db) = setup();
1570        let (rows, _) = query(&db, "FROM blocks WHERE height IN (2, 4)").unwrap();
1571        assert_eq!(ids(&rows), vec!["2", "4"]);
1572    }
1573
1574    #[test]
1575    fn where_in_strings() {
1576        let (_tmp, db) = setup_text();
1577        let (rows, _) = query(&db, r#"FROM jobs WHERE status IN ("open", "closed")"#).unwrap();
1578        assert_eq!(ids(&rows), vec!["1", "3", "4"]);
1579    }
1580
1581    #[test]
1582    fn where_not_in() {
1583        let (_tmp, db) = setup();
1584        let (rows, _) = query(&db, "FROM blocks WHERE height NOT IN (1, 2, 3)").unwrap();
1585        assert_eq!(ids(&rows), vec!["4", "5"]);
1586    }
1587
1588    #[test]
1589    fn where_in_single_value_equals_eq() {
1590        let (_tmp, db) = setup();
1591        let (a, _) = query(&db, "FROM blocks WHERE height IN (3)").unwrap();
1592        let (b, _) = query(&db, "FROM blocks WHERE height = 3").unwrap();
1593        assert_eq!(ids(&a), ids(&b));
1594    }
1595
1596    #[test]
1597    fn where_between_is_inclusive() {
1598        let (_tmp, db) = setup();
1599        let (rows, _) = query(&db, "FROM blocks WHERE height BETWEEN 2 AND 4").unwrap();
1600        // SQL BETWEEN includes both bounds — 2 and 4 must be present.
1601        assert_eq!(ids(&rows), vec!["2", "3", "4"]);
1602    }
1603
1604    #[test]
1605    fn where_not_between() {
1606        let (_tmp, db) = setup();
1607        let (rows, _) = query(&db, "FROM blocks WHERE height NOT BETWEEN 2 AND 4").unwrap();
1608        assert_eq!(ids(&rows), vec!["1", "5"]);
1609    }
1610
1611    /// The AND inside BETWEEN belongs to BETWEEN, not to the conjunction
1612    /// parser. If parse_and grabbed it first, this query would fail to parse
1613    /// or silently lose the second bound.
1614    #[test]
1615    fn between_and_does_not_swallow_the_conjunction() {
1616        let (_tmp, db) = setup();
1617        let (rows, _) = query(
1618            &db, "FROM blocks WHERE height BETWEEN 2 AND 4 AND n_tx > 4").unwrap();
1619        // heights 2,3,4 then n_tx > 4 (n_tx = h*2) leaves 3 and 4.
1620        assert_eq!(ids(&rows), vec!["3", "4"]);
1621    }
1622
1623    #[test]
1624    fn where_like_prefix_suffix_and_infix() {
1625        let (_tmp, db) = setup_text();
1626        let (pre, _) = query(&db, r#"FROM jobs WHERE miner LIKE "Acme%""#).unwrap();
1627        assert_eq!(ids(&pre), vec!["1"]);
1628        let (suf, _) = query(&db, r#"FROM jobs WHERE miner LIKE "%Pool""#).unwrap();
1629        assert_eq!(ids(&suf), vec!["1"]);
1630        let (inf, _) = query(&db, r#"FROM jobs WHERE status LIKE "%pen%""#).unwrap();
1631        assert_eq!(ids(&inf), vec!["1", "2", "4"]);   // open, pending, open
1632    }
1633
1634    #[test]
1635    fn where_like_underscore_matches_exactly_one_char() {
1636        let (_tmp, db) = setup_text();
1637        let (rows, _) = query(&db, r#"FROM jobs WHERE status LIKE "open_""#).unwrap();
1638        assert!(rows.is_empty(), "`open_` must not match the 4-char value `open`");
1639        let (rows2, _) = query(&db, r#"FROM jobs WHERE status LIKE "ope_""#).unwrap();
1640        assert_eq!(ids(&rows2), vec!["1", "4"]);
1641    }
1642
1643    #[test]
1644    fn where_ilike_is_case_insensitive_and_like_is_not() {
1645        let (_tmp, db) = setup_text();
1646        let (ci, _) = query(&db, r#"FROM jobs WHERE miner ILIKE "acme%""#).unwrap();
1647        assert_eq!(ci.len(), 2, "ILIKE matches both `Acme Pool` and `acme solo`");
1648        let (cs, _) = query(&db, r#"FROM jobs WHERE miner LIKE "acme%""#).unwrap();
1649        assert_eq!(ids(&cs), vec!["2"], "LIKE stays case-sensitive");
1650    }
1651
1652    /// The backtracking path: multiple `%` with literals between them, where a
1653    /// greedy first match must be given back for the pattern to succeed.
1654    #[test]
1655    fn like_backtracks_across_multiple_wildcards() {
1656        assert!(like_match("abcabcabd", "%abc%abd", false));
1657        assert!(like_match("aaa", "%a", false));
1658        assert!(like_match("", "%", false));
1659        assert!(like_match("x", "%%%", false));
1660        assert!(!like_match("abc", "%abd", false));
1661        assert!(!like_match("ab", "ab_", false));
1662        assert!(like_match("héllo wörld", "h_llo w%d", false));
1663    }
1664
1665    #[test]
1666    fn where_not_like() {
1667        let (_tmp, db) = setup_text();
1668        let (rows, _) = query(&db, r#"FROM jobs WHERE status NOT LIKE "open""#).unwrap();
1669        assert_eq!(ids(&rows), vec!["2", "3", "5"]);
1670    }
1671
1672    /// NOT LIKE over a NULL/absent field stays false, as in SQL: a predicate
1673    /// over NULL is never true in either polarity. Rows 4 (absent) and 5
1674    /// (explicit null) must appear in NEITHER `LIKE` nor `NOT LIKE`.
1675    #[test]
1676    fn like_over_null_is_false_in_both_polarities() {
1677        let (_tmp, db) = setup_text();
1678        let (pos, _) = query(&db, r#"FROM jobs WHERE miner LIKE "%""#).unwrap();
1679        let (neg, _) = query(&db, r#"FROM jobs WHERE miner NOT LIKE "%""#).unwrap();
1680        assert!(!ids(&pos).contains(&"4".to_string()));
1681        assert!(!ids(&neg).contains(&"4".to_string()));
1682        assert!(!ids(&pos).contains(&"5".to_string()));
1683        assert!(!ids(&neg).contains(&"5".to_string()));
1684    }
1685
1686    /// Absent and explicitly-null are the same observable state in a
1687    /// schemaless store, so IS NULL must catch both.
1688    #[test]
1689    fn where_is_null_catches_absent_and_explicit_null() {
1690        let (_tmp, db) = setup_text();
1691        let (rows, _) = query(&db, "FROM jobs WHERE miner IS NULL").unwrap();
1692        assert_eq!(ids(&rows), vec!["4", "5"]);
1693    }
1694
1695    #[test]
1696    fn where_is_not_null() {
1697        let (_tmp, db) = setup_text();
1698        let (rows, _) = query(&db, "FROM jobs WHERE miner IS NOT NULL").unwrap();
1699        assert_eq!(ids(&rows), vec!["1", "2", "3"]);
1700    }
1701
1702    #[test]
1703    fn where_or() {
1704        let (_tmp, db) = setup();
1705        let (rows, _) = query(&db, "FROM blocks WHERE height = 1 OR height = 5").unwrap();
1706        assert_eq!(ids(&rows), vec!["1", "5"]);
1707    }
1708
1709    /// AND binds tighter than OR, so this is `a OR (b AND c)` and NOT
1710    /// `(a OR b) AND c`. With the wrong precedence the result would be [5].
1711    #[test]
1712    fn and_binds_tighter_than_or() {
1713        let (_tmp, db) = setup();
1714        let (rows, _) = query(
1715            &db, "FROM blocks WHERE height = 1 OR height = 5 AND n_tx = 10").unwrap();
1716        assert_eq!(ids(&rows), vec!["1", "5"]);
1717        let (rows2, _) = query(
1718            &db, "FROM blocks WHERE height = 1 OR height = 5 AND n_tx = 99").unwrap();
1719        assert_eq!(ids(&rows2), vec!["1"], "the AND arm must not match");
1720    }
1721
1722    /// Parentheses must be able to override that precedence.
1723    #[test]
1724    fn parens_override_precedence() {
1725        let (_tmp, db) = setup();
1726        let (rows, _) = query(
1727            &db, "FROM blocks WHERE (height = 1 OR height = 5) AND n_tx = 10").unwrap();
1728        assert_eq!(ids(&rows), vec!["5"]);
1729    }
1730
1731    #[test]
1732    fn nested_parens() {
1733        let (_tmp, db) = setup();
1734        let (rows, _) = query(
1735            &db,
1736            "FROM blocks WHERE ((height >= 2 AND height <= 4) OR height = 1) AND n_tx != 6",
1737        ).unwrap();
1738        assert_eq!(ids(&rows), vec!["1", "2", "4"]);
1739    }
1740
1741    #[test]
1742    fn not_negates_a_group() {
1743        let (_tmp, db) = setup();
1744        let (rows, _) = query(&db, "FROM blocks WHERE NOT (height > 2)").unwrap();
1745        assert_eq!(ids(&rows), vec!["1", "2"]);
1746    }
1747
1748    /// Prefix NOT before a bare comparison, as SQL allows. Distinct from the
1749    /// INFIX `field NOT <op>` form, which is a syntax error — only NOT IN /
1750    /// NOT BETWEEN / NOT LIKE exist in that position.
1751    #[test]
1752    fn prefix_not_before_a_comparison() {
1753        let (_tmp, db) = setup();
1754        let (rows, _) = query(&db, "FROM blocks WHERE NOT height = 1").unwrap();
1755        assert_eq!(ids(&rows), vec!["2", "3", "4", "5"]);
1756        let (double, _) = query(&db, "FROM blocks WHERE NOT NOT height = 1").unwrap();
1757        assert_eq!(ids(&double), vec!["1"]);
1758        let (mixed, _) = query(&db, "FROM blocks WHERE NOT height = 1 AND height < 4").unwrap();
1759        assert_eq!(ids(&mixed), vec!["2", "3"]);
1760    }
1761
1762    /// `_id = "x"` takes an O(1) index path. Under an OR it does not constrain
1763    /// the result set, so using it as a point lookup would drop every row the
1764    /// other arm matched. Guards the id_point_lookup AND-only descent.
1765    #[test]
1766    fn id_equality_under_or_does_not_become_a_point_lookup() {
1767        let (_tmp, db) = setup();
1768        let (rows, _) = query(&db, r#"FROM blocks WHERE _id = "1" OR height > 3"#).unwrap();
1769        assert_eq!(ids(&rows), vec!["1", "4", "5"],
1770                   "the OR arm must survive the id fast path");
1771    }
1772
1773    /// The fast path is still taken when the equality is a genuine conjunct.
1774    #[test]
1775    fn id_equality_under_and_still_point_looks_up() {
1776        let (_tmp, db) = setup();
1777        let (hit, _) = query(&db, r#"FROM blocks WHERE _id = "3" AND n_tx = 6"#).unwrap();
1778        assert_eq!(ids(&hit), vec!["3"]);
1779        let (miss, _) = query(&db, r#"FROM blocks WHERE _id = "3" AND n_tx = 999"#).unwrap();
1780        assert!(miss.is_empty(), "the second conjunct must still be applied");
1781    }
1782
1783    #[test]
1784    fn metadata_fields_are_filterable() {
1785        let (_tmp, db) = setup();
1786        // _seq is 0-indexed — the first put lands at seq 0, so `> 0` drops it.
1787        let (rows, _) = query(&db, "FROM blocks WHERE _seq >= 0 AND _coll = blocks").unwrap();
1788        assert_eq!(rows.len(), 5);
1789        let (tail, _) = query(&db, "FROM blocks WHERE _seq > 0").unwrap();
1790        assert_eq!(tail.len(), 4);
1791    }
1792
1793    #[test]
1794    fn combined_with_order_and_limit() {
1795        let (_tmp, db) = setup();
1796        let (rows, _) = query(
1797            &db,
1798            "FROM blocks WHERE height IN (1, 3, 5) ORDER BY height DESC LIMIT 2",
1799        ).unwrap();
1800        let heights: Vec<u64> = rows.iter().filter_map(|r| r["height"].as_u64()).collect();
1801        assert_eq!(heights, vec![5, 3]);
1802    }
1803
1804    // ── Strictness: a query the engine cannot honour must FAIL, not lie ──────
1805
1806    /// The headline regression. `_ => { self.advance(); }` meant an
1807    /// unimplemented or misspelled clause was dropped and a DIFFERENT query
1808    /// was answered. Each of these previously returned rows.
1809    #[test]
1810    fn unknown_clauses_are_errors_not_silent_skips() {
1811        let (_tmp, db) = setup();
1812        for bad in [
1813            "FROM blocks ORDRE BY height",       // typo
1814            "FROM blocks WHERE height > 3 JUNK", // trailing garbage
1815            "FROM blocks SELECT height",         // wrong dialect
1816            "FROM blocks LIMIT",                 // missing count
1817            "FROM blocks OFFSET",                // missing count
1818            "FROM blocks ORDER BY",              // missing key
1819            "FROM blocks ORDER BY height,",      // trailing comma
1820        ] {
1821            assert!(query(&db, bad).is_err(), "`{}` must be rejected, not silently reinterpreted", bad);
1822        }
1823    }
1824
1825    #[test]
1826    fn malformed_predicates_are_errors() {
1827        let (_tmp, db) = setup();
1828        for bad in [
1829            "FROM blocks WHERE height >",            // missing value
1830            "FROM blocks WHERE height IN (",         // unterminated list
1831            "FROM blocks WHERE height IN ()",        // empty list
1832            "FROM blocks WHERE height BETWEEN 1",    // missing AND high
1833            "FROM blocks WHERE height BETWEEN 1 3",  // missing AND
1834            "FROM blocks WHERE (height = 1",         // unbalanced paren
1835            "FROM blocks WHERE height IS 3",         // IS without NULL
1836            "FROM blocks WHERE height NOT = 1",      // infix NOT before a comparison op
1837            "FROM blocks WHERE height LIKE",         // missing pattern
1838        ] {
1839            assert!(query(&db, bad).is_err(), "`{}` must be a parse error", bad);
1840        }
1841    }
1842
1843    /// ASC used to survive only because unknown tokens were skipped. Now that
1844    /// skipping is gone it has to be a real keyword, and the pre-existing
1845    /// `order_by_limit` test above depends on it.
1846    #[test]
1847    fn asc_is_accepted_explicitly() {
1848        let (_tmp, db) = setup();
1849        let (asc, _) = query(&db, "FROM blocks ORDER BY height ASC").unwrap();
1850        let (plain, _) = query(&db, "FROM blocks ORDER BY height").unwrap();
1851        assert_eq!(asc[0]["height"], 1);
1852        assert_eq!(plain[0]["height"], 1);
1853    }
1854
1855    /// Lowercase and mixed-case keywords must keep working — the lexer
1856    /// uppercases before matching, and the new keywords must be no different.
1857    #[test]
1858    fn new_keywords_are_case_insensitive() {
1859        let (_tmp, db) = setup();
1860        let (rows, _) = query(&db, "from blocks where height between 2 and 3").unwrap();
1861        assert_eq!(ids(&rows), vec!["2", "3"]);
1862        let (rows2, _) = query(&db, "FROM blocks Where height In (1) Or height In (2)").unwrap();
1863        assert_eq!(ids(&rows2), vec!["1", "2"]);
1864    }
1865
1866    #[test]
1867    fn group_by_count() {
1868        let (_tmp, db) = setup();
1869        let (rows, _) = query(&db, "FROM blocks GROUP BY n_tx COUNT").unwrap();
1870        assert_eq!(rows.len(), 5); // all unique n_tx values
1871    }
1872
1873    // ── Indexed range / point scans (3.3.0) ─────────────────────────────────
1874    //
1875    // The load-bearing property is EQUIVALENCE: an indexed query and the same
1876    // query without an index must return the same rows. The planner is allowed
1877    // to be imprecise (it only has to produce a superset — the full predicate
1878    // re-runs on the candidates) but it is never allowed to be wrong.
1879    //
1880    // Every test below therefore runs the same query against two databases
1881    // holding identical data, one indexed and one not, and compares.
1882
1883    /// Build two identical databases, one with sorted indexes on `fields`.
1884    fn twin(fields: &[&str]) -> (tempfile::TempDir, tempfile::TempDir, Db, Db) {
1885        let d1 = tempdir().unwrap();
1886        let d2 = tempdir().unwrap();
1887        let indexed = Db::open(d1.path(), None).unwrap();
1888        let plain = Db::open(d2.path(), None).unwrap();
1889        for f in fields {
1890            indexed.create_sorted_index("t", f);
1891        }
1892        // Deliberately messy: duplicate fees, a missing field, a null, a
1893        // string column, and an out-of-order insert sequence.
1894        let rows: Vec<(String, Value)> = (0..40u64).map(|i| {
1895            let mut o = serde_json::Map::new();
1896            if i % 7 != 0 {
1897                o.insert("fee".into(), json!(i % 13));
1898            }
1899            if i % 11 == 0 {
1900                o.insert("note".into(), Value::Null);
1901            } else {
1902                o.insert("note".into(), json!(format!("n{}", i % 5)));
1903            }
1904            o.insert("rank".into(), json!(40 - i));
1905            (i.to_string(), Value::Object(o))
1906        }).collect();
1907        for (id, doc) in &rows {
1908            indexed.put("t", id, doc.clone(), vec![], None, None).unwrap();
1909            plain.put("t", id, doc.clone(), vec![], None, None).unwrap();
1910        }
1911        (d1, d2, indexed, plain)
1912    }
1913
1914    fn same(a: &Db, b: &Db, nql: &str) -> (Vec<String>, Vec<String>) {
1915        let ga = {
1916            let (rows, _) = query(a, nql).unwrap();
1917            let mut v: Vec<String> = rows.iter()
1918                .filter_map(|r| r["_id"].as_str().map(String::from)).collect();
1919            v.sort(); v
1920        };
1921        let gb = {
1922            let (rows, _) = query(b, nql).unwrap();
1923            let mut v: Vec<String> = rows.iter()
1924                .filter_map(|r| r["_id"].as_str().map(String::from)).collect();
1925            v.sort(); v
1926        };
1927        (ga, gb)
1928    }
1929
1930    #[test]
1931    fn indexed_and_unindexed_agree_on_every_predicate_shape() {
1932        let (_t1, _t2, idx, plain) = twin(&["fee", "note", "rank"]);
1933        for nql in [
1934            // ranges — the shapes the index now serves
1935            "FROM t WHERE fee > 5",
1936            "FROM t WHERE fee >= 5",
1937            "FROM t WHERE fee < 5",
1938            "FROM t WHERE fee <= 5",
1939            "FROM t WHERE fee = 5",
1940            "FROM t WHERE fee BETWEEN 3 AND 8",
1941            "FROM t WHERE fee NOT BETWEEN 3 AND 8",
1942            "FROM t WHERE fee IN (1, 5, 9)",
1943            "FROM t WHERE fee NOT IN (1, 5, 9)",
1944            "FROM t WHERE fee != 5",
1945            // merged bounds on one field
1946            "FROM t WHERE fee > 3 AND fee < 9",
1947            "FROM t WHERE fee >= 3 AND fee <= 9",
1948            "FROM t WHERE fee > 3 AND fee < 9 AND fee != 5",
1949            "FROM t WHERE fee BETWEEN 2 AND 10 AND fee > 6",
1950            // two indexed fields — the planner must pick one and stay correct
1951            "FROM t WHERE fee > 5 AND rank < 20",
1952            "FROM t WHERE fee IN (2, 3) AND rank > 10",
1953            "FROM t WHERE fee = 4 AND rank = 8",
1954            // the absent-field cases, where a naive index scan inverts the answer
1955            "FROM t WHERE fee IS NULL",
1956            "FROM t WHERE fee IS NOT NULL",
1957            "FROM t WHERE note IS NULL",
1958            "FROM t WHERE note IS NOT NULL",
1959            "FROM t WHERE fee IS NULL AND rank > 20",
1960            // predicates the index cannot serve, mixed with ones it can
1961            r#"FROM t WHERE note LIKE "n_""#,
1962            r#"FROM t WHERE fee > 5 AND note LIKE "n1""#,
1963            r#"FROM t WHERE note NOT LIKE "n1" AND fee < 4"#,
1964            // disjunction — must NOT be narrowed on one arm
1965            "FROM t WHERE fee > 11 OR rank > 38",
1966            "FROM t WHERE fee = 1 OR note IS NULL",
1967            "FROM t WHERE (fee > 11 OR rank > 38) AND rank < 39",
1968            "FROM t WHERE fee IN (1) OR fee IN (2)",
1969            // negation
1970            "FROM t WHERE NOT (fee > 5)",
1971            "FROM t WHERE NOT (fee IN (1, 2))",
1972            "FROM t WHERE NOT (fee > 5) AND rank < 30",
1973            // with shaping on top
1974            "FROM t WHERE fee > 5 ORDER BY rank DESC LIMIT 5",
1975            "FROM t WHERE fee BETWEEN 2 AND 8 ORDER BY fee, rank DESC",
1976            "FROM t WHERE fee > 5 GROUP BY note COUNT",
1977            "FROM t WHERE fee > 5 COUNT",
1978            "FROM t WHERE fee > 5 ORDER BY rank LIMIT 3 OFFSET 2",
1979            // empty results
1980            "FROM t WHERE fee > 9999",
1981            "FROM t WHERE fee IN (9999)",
1982            "FROM t WHERE fee BETWEEN 100 AND 200",
1983        ] {
1984            let (a, b) = same(&idx, &plain, nql);
1985            assert_eq!(a, b, "indexed and unindexed disagree on `{}`", nql);
1986        }
1987    }
1988
1989    /// Ordering, not just membership, must survive the index path — the
1990    /// candidates arrive in index order of the PREDICATE field, which is not
1991    /// the requested sort order, so the post-filter sort has to still run.
1992    #[test]
1993    fn index_path_still_honours_order_by() {
1994        let (_t1, _t2, idx, plain) = twin(&["fee", "rank"]);
1995        for nql in [
1996            "FROM t WHERE fee > 4 ORDER BY rank",
1997            "FROM t WHERE fee > 4 ORDER BY rank DESC",
1998            "FROM t WHERE fee > 4 ORDER BY note, rank DESC",
1999            "FROM t WHERE fee BETWEEN 2 AND 9 ORDER BY rank LIMIT 4",
2000            "FROM t WHERE fee IN (3, 6) ORDER BY rank DESC LIMIT 2",
2001        ] {
2002            let ra = query(&idx, nql).unwrap().0;
2003            let rb = query(&plain, nql).unwrap().0;
2004            let ia: Vec<&str> = ra.iter().filter_map(|r| r["_id"].as_str()).collect();
2005            let ib: Vec<&str> = rb.iter().filter_map(|r| r["_id"].as_str()).collect();
2006            assert_eq!(ia, ib, "row ORDER differs on `{}`", nql);
2007        }
2008    }
2009
2010    /// An ordering comparison against a missing field is never true.
2011    ///
2012    /// OrderedValue sorts Null below every number, so `<` and `<=` reported
2013    /// that a document with NO `fee` field satisfied `WHERE fee < 5` — while
2014    /// `>` and `>=` excluded it. That asymmetry was the tell. The Python
2015    /// reference has always excluded it, so this was a cross-engine
2016    /// divergence as well as a wrong answer, and it meant the scan path and
2017    /// the index path disagreed depending on whether an index existed.
2018    #[test]
2019    fn an_ordering_comparison_against_a_missing_field_is_false() {
2020        let dir = tempdir().unwrap();
2021        let db = Db::open(dir.path(), None).unwrap();
2022        db.put("t", "has", json!({"fee": 1}), vec![], None, None).unwrap();
2023        db.put("t", "none", json!({"other": 1}), vec![], None, None).unwrap();
2024        db.put("t", "null", json!({"fee": Value::Null}), vec![], None, None).unwrap();
2025
2026        for nql in ["FROM t WHERE fee < 5", "FROM t WHERE fee <= 5"] {
2027            let (r, _) = query(&db, nql).unwrap();
2028            let ids: Vec<&str> = r.iter().filter_map(|x| x["_id"].as_str()).collect();
2029            assert_eq!(ids, vec!["has"],
2030                       "`{}` must not match a row whose fee is absent or null", nql);
2031        }
2032        for nql in ["FROM t WHERE fee > 0", "FROM t WHERE fee >= 0"] {
2033            let (r, _) = query(&db, nql).unwrap();
2034            let ids: Vec<&str> = r.iter().filter_map(|x| x["_id"].as_str()).collect();
2035            assert_eq!(ids, vec!["has"], "`{}`", nql);
2036        }
2037        // BETWEEN is built from >= and <=, so it inherits the rule.
2038        let (b, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 9").unwrap();
2039        assert_eq!(b.len(), 1);
2040
2041        // = and != keep operating on null, exactly as the Python reference
2042        // does — its None guard sits deliberately AFTER those two arms.
2043        let (ne, _) = query(&db, "FROM t WHERE fee != 5").unwrap();
2044        assert_eq!(ne.len(), 3, "!= still matches absent and null fields");
2045        let (isnull, _) = query(&db, "FROM t WHERE fee = NULL").unwrap();
2046        assert_eq!(isnull.len(), 2, "absent and explicit-null both equal NULL");
2047
2048        // And the same answers with an index present — the two paths agreeing
2049        // is the reason this fix was required, not merely desirable.
2050        let d2 = tempdir().unwrap();
2051        let idx = Db::open(d2.path(), None).unwrap();
2052        idx.create_sorted_index("t", "fee");
2053        idx.put("t", "has", json!({"fee": 1}), vec![], None, None).unwrap();
2054        idx.put("t", "none", json!({"other": 1}), vec![], None, None).unwrap();
2055        idx.put("t", "null", json!({"fee": Value::Null}), vec![], None, None).unwrap();
2056        for nql in ["FROM t WHERE fee < 5", "FROM t WHERE fee <= 5",
2057                    "FROM t WHERE fee > 0", "FROM t WHERE fee BETWEEN 0 AND 9"] {
2058            let (a, _) = query(&db, nql).unwrap();
2059            let (b, _) = query(&idx, nql).unwrap();
2060            let ia: Vec<&str> = a.iter().filter_map(|x| x["_id"].as_str()).collect();
2061            let ib: Vec<&str> = b.iter().filter_map(|x| x["_id"].as_str()).collect();
2062            assert_eq!(ia, ib, "indexed and unindexed disagree on `{}`", nql);
2063        }
2064    }
2065
2066    /// `IS NULL` must never touch this index. A document whose field is absent
2067    /// is not in the index for that field, so an index scan would return
2068    /// exactly the complement of the right answer — the worst possible failure
2069    /// for a filter, since it looks like a plausible result set.
2070    #[test]
2071    fn is_null_never_uses_the_index() {
2072        let (_t1, _t2, idx, plain) = twin(&["fee"]);
2073        let (a, b) = same(&idx, &plain, "FROM t WHERE fee IS NULL");
2074        assert_eq!(a, b);
2075        // 40 docs, every 7th missing `fee`: ids 0,7,14,21,28,35.
2076        assert_eq!(a, vec!["0", "14", "21", "28", "35", "7"]);
2077        assert!(!a.is_empty(), "the fixture must actually contain absent fields");
2078    }
2079
2080    /// A constraint under an OR does not restrict the result set, so the
2081    /// planner must not narrow on it. Both arms have to survive.
2082    #[test]
2083    fn a_disjunct_is_never_used_to_narrow() {
2084        let (_t1, _t2, idx, plain) = twin(&["fee", "rank"]);
2085        let nql = "FROM t WHERE fee = 1 OR rank = 40";
2086        let (a, b) = same(&idx, &plain, nql);
2087        assert_eq!(a, b);
2088        // rank = 40 is doc 0, which has NO `fee` field at all — so if the
2089        // planner had narrowed on the `fee` arm it would have been dropped.
2090        assert!(a.contains(&"0".to_string()),
2091                "the OR arm matching a doc with no indexed field must survive: {:?}", a);
2092        assert!(a.len() > 1, "both arms must contribute: {:?}", a);
2093    }
2094
2095    /// AS OF must not use the index: it holds CURRENT versions only, because a
2096    /// superseded hash is removed on overwrite. An index scan would answer a
2097    /// historical query with present-day rows.
2098    #[test]
2099    fn as_of_does_not_use_the_current_version_index() {
2100        let dir = tempdir().unwrap();
2101        let db = Db::open(dir.path(), None).unwrap();
2102        db.create_sorted_index("t", "fee");
2103        db.put("t", "a", json!({"fee": 5}), vec![], None, None).unwrap();
2104        let snap = db.put("t", "b", json!({"fee": 5}), vec![], None, None).unwrap().seq;
2105        // Move both out of the range the query asks for.
2106        db.put("t", "a", json!({"fee": 999}), vec![], None, None).unwrap();
2107        db.put("t", "b", json!({"fee": 999}), vec![], None, None).unwrap();
2108
2109        // At HEAD nothing matches fee = 5 any more.
2110        let (now, _) = query(&db, "FROM t WHERE fee = 5").unwrap();
2111        assert!(now.is_empty(), "current versions have fee 999: {:?}", now);
2112
2113        // AS OF the snapshot, both still had fee = 5. If the index served
2114        // this, it would return nothing.
2115        let (then, _) = query(&db, &format!("FROM t AS OF {} WHERE fee = 5", snap)).unwrap();
2116        let mut ids: Vec<&str> = then.iter().filter_map(|r| r["_id"].as_str()).collect();
2117        ids.sort();
2118        assert_eq!(ids, vec!["a", "b"], "AS OF must see the historical values");
2119
2120        // Same for a range and an IN.
2121        let (r, _) = query(&db, &format!("FROM t AS OF {} WHERE fee BETWEEN 1 AND 9", snap)).unwrap();
2122        assert_eq!(r.len(), 2);
2123        let (i, _) = query(&db, &format!("FROM t AS OF {} WHERE fee IN (5)", snap)).unwrap();
2124        assert_eq!(i.len(), 2);
2125    }
2126
2127    /// An overwritten row must not come back from the index.
2128    #[test]
2129    fn the_index_path_returns_current_versions_only() {
2130        let dir = tempdir().unwrap();
2131        let db = Db::open(dir.path(), None).unwrap();
2132        db.create_sorted_index("t", "fee");
2133        for i in 0..5u64 {
2134            db.put("t", &i.to_string(), json!({"fee": i}), vec![], None, None).unwrap();
2135        }
2136        db.put("t", "0", json!({"fee": 100}), vec![], None, None).unwrap();
2137
2138        let (low, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 4").unwrap();
2139        let mut ids: Vec<&str> = low.iter().filter_map(|r| r["_id"].as_str()).collect();
2140        ids.sort();
2141        assert_eq!(ids, vec!["1", "2", "3", "4"],
2142                   "doc 0 moved to fee 100 and must not appear in 0..4");
2143
2144        let (high, _) = query(&db, "FROM t WHERE fee = 100").unwrap();
2145        assert_eq!(high.len(), 1);
2146        assert_eq!(high[0]["_id"], "0");
2147        assert_eq!(high[0]["fee"], json!(100), "the CURRENT value, not the old one");
2148    }
2149
2150    /// Duplicate values must not produce duplicate rows, and a value repeated
2151    /// across IN arms must be returned once.
2152    #[test]
2153    fn index_scans_do_not_duplicate_rows() {
2154        let dir = tempdir().unwrap();
2155        let db = Db::open(dir.path(), None).unwrap();
2156        db.create_sorted_index("t", "fee");
2157        for i in 0..6u64 {
2158            db.put("t", &i.to_string(), json!({"fee": i % 2}), vec![], None, None).unwrap();
2159        }
2160        let (dup, _) = query(&db, "FROM t WHERE fee IN (0, 0, 1, 1)").unwrap();
2161        assert_eq!(dup.len(), 6, "each row once despite repeated IN arms");
2162        let (r, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 1").unwrap();
2163        assert_eq!(r.len(), 6);
2164        let mut ids: Vec<&str> = dup.iter().filter_map(|r| r["_id"].as_str()).collect();
2165        ids.sort();
2166        ids.dedup();
2167        assert_eq!(ids.len(), 6, "no duplicate _ids");
2168    }
2169
2170    /// A range over a string column, to prove the index is not numeric-only.
2171    #[test]
2172    fn index_ranges_work_on_strings() {
2173        let dir = tempdir().unwrap();
2174        let db = Db::open(dir.path(), None).unwrap();
2175        db.create_sorted_index("t", "name");
2176        for (i, n) in ["alpha", "bravo", "charlie", "delta", "echo"].iter().enumerate() {
2177            db.put("t", &i.to_string(), json!({"name": n}), vec![], None, None).unwrap();
2178        }
2179        let (r, _) = query(&db, r#"FROM t WHERE name BETWEEN "bravo" AND "delta""#).unwrap();
2180        let mut got: Vec<&str> = r.iter().filter_map(|x| x["name"].as_str()).collect();
2181        got.sort();
2182        assert_eq!(got, vec!["bravo", "charlie", "delta"]);
2183        let (gt, _) = query(&db, r#"FROM t WHERE name > "charlie""#).unwrap();
2184        assert_eq!(gt.len(), 2);
2185    }
2186
2187    /// Bounds must be merged into one walk, and the tighter bound must win
2188    /// regardless of the order the conjuncts appear in.
2189    #[test]
2190    fn same_field_bounds_are_merged_tightest_wins() {
2191        let (_t1, _t2, idx, plain) = twin(&["fee"]);
2192        for (a_nql, b_nql) in [
2193            ("FROM t WHERE fee > 2 AND fee > 6", "FROM t WHERE fee > 6"),
2194            ("FROM t WHERE fee > 6 AND fee > 2", "FROM t WHERE fee > 6"),
2195            ("FROM t WHERE fee < 9 AND fee < 4", "FROM t WHERE fee < 4"),
2196            ("FROM t WHERE fee BETWEEN 0 AND 12 AND fee >= 5 AND fee <= 7",
2197             "FROM t WHERE fee >= 5 AND fee <= 7"),
2198        ] {
2199            let (ia, _) = same(&idx, &plain, a_nql);
2200            let (ib, _) = same(&idx, &plain, b_nql);
2201            assert_eq!(ia, ib, "`{}` should equal `{}`", a_nql, b_nql);
2202        }
2203    }
2204
2205    /// The index only helps where it exists; an unindexed field must still
2206    /// answer correctly through the scan path.
2207    #[test]
2208    fn a_predicate_on_an_unindexed_field_still_answers() {
2209        let (_t1, _t2, idx, plain) = twin(&["fee"]);   // `rank` is NOT indexed
2210        for nql in [
2211            "FROM t WHERE rank > 30",
2212            "FROM t WHERE rank BETWEEN 10 AND 20",
2213            "FROM t WHERE rank IN (40, 39)",
2214            "FROM t WHERE rank > 30 AND fee > 2",
2215        ] {
2216            let (a, b) = same(&idx, &plain, nql);
2217            assert_eq!(a, b, "`{}`", nql);
2218        }
2219    }
2220
2221    /// Cardinality is reported off the index without reading any rows, which
2222    /// is what lets the planner compare two candidate indexes.
2223    #[test]
2224    fn range_cardinality_counts_without_reading() {
2225        let dir = tempdir().unwrap();
2226        let db = Db::open(dir.path(), None).unwrap();
2227        db.create_sorted_index("t", "fee");
2228        for i in 0..20u64 {
2229            db.put("t", &i.to_string(), json!({"fee": i}), vec![], None, None).unwrap();
2230        }
2231        assert_eq!(db.range_cardinality("t", "fee", None, None, true, true), Some(20));
2232        assert_eq!(
2233            db.range_cardinality("t", "fee", Some(&json!(5)), Some(&json!(9)), true, true),
2234            Some(5), "5..=9 inclusive is five values");
2235        assert_eq!(
2236            db.range_cardinality("t", "fee", Some(&json!(5)), Some(&json!(9)), false, false),
2237            Some(3), "exclusive bounds drop both ends");
2238        assert_eq!(
2239            db.range_cardinality("t", "fee", Some(&json!(18)), None, true, true),
2240            Some(2));
2241        assert_eq!(
2242            db.range_cardinality("t", "fee", Some(&json!(999)), None, true, true),
2243            Some(0), "an empty range is 0, not an error");
2244        // No index on this field at all.
2245        assert_eq!(db.range_cardinality("t", "nope", None, None, true, true), None);
2246    }
2247
2248    /// With two usable indexes the planner should choose the narrower range.
2249    /// Asserted through cardinality rather than by inspecting the plan, so the
2250    /// test pins the observable behaviour and not the implementation.
2251    #[test]
2252    fn the_narrower_index_is_preferred() {
2253        let dir = tempdir().unwrap();
2254        let db = Db::open(dir.path(), None).unwrap();
2255        db.create_sorted_index("t", "wide");
2256        db.create_sorted_index("t", "narrow");
2257        for i in 0..100u64 {
2258            db.put("t", &i.to_string(),
2259                   json!({"wide": i % 2, "narrow": i}), vec![], None, None).unwrap();
2260        }
2261        // `wide = 0` covers 50 rows; `narrow = 7` covers 1.
2262        let wide = db.range_cardinality("t", "wide", Some(&json!(0)), Some(&json!(0)), true, true);
2263        let narrow = db.range_cardinality("t", "narrow", Some(&json!(7)), Some(&json!(7)), true, true);
2264        assert_eq!(wide, Some(50));
2265        assert_eq!(narrow, Some(1));
2266        // The answer must be right whichever index is chosen.
2267        let (r, _) = query(&db, "FROM t WHERE wide = 0 AND narrow = 7").unwrap();
2268        assert!(r.is_empty(), "narrow 7 has wide 1, so nothing matches");
2269        let (r2, _) = query(&db, "FROM t WHERE wide = 0 AND narrow = 8").unwrap();
2270        assert_eq!(r2.len(), 1);
2271        assert_eq!(r2[0]["_id"], "8");
2272    }
2273
2274    // ── Result shaping (3.3.0): OFFSET, multi-key ORDER BY, HAVING, ─────────
2275    // ── bare aggregates, and the SQL pipeline order ─────────────────────────
2276
2277    fn heights(rows: &[Value]) -> Vec<u64> {
2278        rows.iter().filter_map(|r| r["height"].as_u64()).collect()
2279    }
2280
2281    #[test]
2282    fn offset_skips_result_rows() {
2283        let (_tmp, db) = setup();
2284        let (rows, _) = query(&db, "FROM blocks ORDER BY height OFFSET 2").unwrap();
2285        assert_eq!(heights(&rows), vec![3, 4, 5]);
2286    }
2287
2288    #[test]
2289    fn offset_with_limit_pages() {
2290        let (_tmp, db) = setup();
2291        // Page through 5 rows two at a time. Each page must be disjoint and
2292        // in order — the bug to catch is a pushdown that fetches only `limit`
2293        // rows and therefore returns page 1 for every page.
2294        let mut seen = vec![];
2295        for page in 0..3 {
2296            let (rows, _) = query(
2297                &db,
2298                &format!("FROM blocks ORDER BY height LIMIT 2 OFFSET {}", page * 2),
2299            ).unwrap();
2300            seen.extend(heights(&rows));
2301        }
2302        assert_eq!(seen, vec![1, 2, 3, 4, 5]);
2303    }
2304
2305    #[test]
2306    fn offset_past_the_end_is_an_empty_page() {
2307        let (_tmp, db) = setup();
2308        let (rows, count) = query(&db, "FROM blocks OFFSET 99").unwrap();
2309        assert!(rows.is_empty());
2310        assert_eq!(count, 0);
2311        let (zero, _) = query(&db, "FROM blocks OFFSET 0").unwrap();
2312        assert_eq!(zero.len(), 5, "OFFSET 0 skips nothing");
2313    }
2314
2315    #[test]
2316    fn offset_applies_after_the_filter() {
2317        let (_tmp, db) = setup();
2318        // n_tx = h*2, so `>= 6` matches heights 3,4,5. Offsetting by one must
2319        // skip the first MATCH, not the first row of the collection.
2320        let (rows, _) = query(
2321            &db, "FROM blocks WHERE n_tx >= 6 ORDER BY height OFFSET 1").unwrap();
2322        assert_eq!(heights(&rows), vec![4, 5]);
2323    }
2324
2325    #[test]
2326    fn order_by_multiple_keys() {
2327        let dir = tempdir().unwrap();
2328        let db = Db::open(dir.path(), None).unwrap();
2329        // Two statuses, each with several fees, so the second key has to do
2330        // real work to break the first key's ties.
2331        for (i, (s, f)) in [("open", 30), ("open", 10), ("closed", 20),
2332                            ("open", 20), ("closed", 5)].iter().enumerate() {
2333            db.put("t", &i.to_string(),
2334                serde_json::json!({"status": s, "fee": f}), vec![], None, None).unwrap();
2335        }
2336        let (rows, _) = query(&db, "FROM t ORDER BY status, fee DESC").unwrap();
2337        let got: Vec<(String, u64)> = rows.iter()
2338            .map(|r| (r["status"].as_str().unwrap().to_string(), r["fee"].as_u64().unwrap()))
2339            .collect();
2340        assert_eq!(got, vec![
2341            ("closed".into(), 20), ("closed".into(), 5),
2342            ("open".into(), 30), ("open".into(), 20), ("open".into(), 10),
2343        ]);
2344    }
2345
2346    #[test]
2347    fn order_by_mixed_directions() {
2348        let dir = tempdir().unwrap();
2349        let db = Db::open(dir.path(), None).unwrap();
2350        for (i, (a, b)) in [(1, 1), (1, 2), (2, 1), (2, 2)].iter().enumerate() {
2351            db.put("t", &i.to_string(),
2352                serde_json::json!({"a": a, "b": b}), vec![], None, None).unwrap();
2353        }
2354        let (rows, _) = query(&db, "FROM t ORDER BY a DESC, b ASC").unwrap();
2355        let got: Vec<(u64, u64)> = rows.iter()
2356            .map(|r| (r["a"].as_u64().unwrap(), r["b"].as_u64().unwrap()))
2357            .collect();
2358        assert_eq!(got, vec![(2, 1), (2, 2), (1, 1), (1, 2)]);
2359    }
2360
2361    /// The headline pipeline-order bug. In SQL, LIMIT applies to the RESULT.
2362    /// The old order was ORDER BY -> LIMIT -> GROUP BY, so LIMIT truncated the
2363    /// INPUT and the aggregate was computed over a fraction of the rows —
2364    /// reporting counts that summed to the limit instead of the true total.
2365    #[test]
2366    fn limit_applies_to_grouped_rows_not_to_the_input() {
2367        let dir = tempdir().unwrap();
2368        let db = Db::open(dir.path(), None).unwrap();
2369        for i in 0..12 {
2370            // Hoisted: json! cannot parse an indexing expression inline.
2371            let status = ["open", "closed", "void"][i % 3];
2372            db.put("t", &i.to_string(),
2373                serde_json::json!({"status": status, "fee": i}),
2374                vec![], None, None).unwrap();
2375        }
2376        let (all, _) = query(&db, "FROM t GROUP BY status COUNT").unwrap();
2377        assert_eq!(all.len(), 3);
2378        let total: u64 = all.iter().filter_map(|r| r["count"].as_u64()).sum();
2379        assert_eq!(total, 12, "every input row must be counted");
2380
2381        // LIMIT 2 must return 2 GROUPS, each with its full count — not two
2382        // input rows regrouped.
2383        let (limited, _) = query(&db, "FROM t GROUP BY status COUNT LIMIT 2").unwrap();
2384        assert_eq!(limited.len(), 2, "LIMIT caps the number of groups");
2385        for r in &limited {
2386            assert_eq!(r["count"], json!(4),
2387                       "each group keeps its true count, got {:?}", r);
2388        }
2389    }
2390
2391    /// The second pipeline-order bug: ORDER BY ran before grouping, so it
2392    /// sorted the raw documents on a field that only exists AFTER grouping
2393    /// (`count`, `sum_fee`) and the grouped output came back unordered. The
2394    /// clause was silently inert.
2395    #[test]
2396    fn order_by_sorts_the_grouped_rows() {
2397        let dir = tempdir().unwrap();
2398        let db = Db::open(dir.path(), None).unwrap();
2399        // Deliberately uneven: 1 x "a", 3 x "b", 2 x "c".
2400        for (i, s) in ["a", "b", "b", "b", "c", "c"].iter().enumerate() {
2401            db.put("t", &i.to_string(),
2402                serde_json::json!({"g": s, "n": i}), vec![], None, None).unwrap();
2403        }
2404        let (rows, _) = query(&db, "FROM t GROUP BY g COUNT ORDER BY count DESC").unwrap();
2405        let got: Vec<(String, u64)> = rows.iter()
2406            .map(|r| (r["g"].as_str().unwrap().to_string(), r["count"].as_u64().unwrap()))
2407            .collect();
2408        assert_eq!(got, vec![("b".into(), 3), ("c".into(), 2), ("a".into(), 1)]);
2409
2410        // And the group key itself is sortable.
2411        let (by_key, _) = query(&db, "FROM t GROUP BY g COUNT ORDER BY g DESC").unwrap();
2412        let keys: Vec<&str> = by_key.iter().map(|r| r["g"].as_str().unwrap()).collect();
2413        assert_eq!(keys, vec!["c", "b", "a"]);
2414    }
2415
2416    #[test]
2417    fn order_by_an_aggregate_key() {
2418        let (_tmp, db) = setup_items();
2419        let (rows, _) = query(
2420            &db, "FROM items GROUP BY cat SUM price ORDER BY sum_price DESC").unwrap();
2421        let cats: Vec<&str> = rows.iter().map(|r| r["cat"].as_str().unwrap()).collect();
2422        assert_eq!(cats, vec!["y", "x"], "y sums to 60, x to 15");
2423    }
2424
2425    #[test]
2426    fn offset_and_limit_page_grouped_rows() {
2427        let dir = tempdir().unwrap();
2428        let db = Db::open(dir.path(), None).unwrap();
2429        for i in 0..9 {
2430            db.put("t", &i.to_string(),
2431                serde_json::json!({"g": format!("g{}", i % 3)}), vec![], None, None).unwrap();
2432        }
2433        let (page, _) = query(
2434            &db, "FROM t GROUP BY g COUNT ORDER BY g LIMIT 1 OFFSET 1").unwrap();
2435        assert_eq!(page.len(), 1);
2436        assert_eq!(page[0]["g"], "g1");
2437    }
2438
2439    // ── HAVING ──────────────────────────────────────────────────────────────
2440
2441    #[test]
2442    fn having_filters_groups_by_count() {
2443        let dir = tempdir().unwrap();
2444        let db = Db::open(dir.path(), None).unwrap();
2445        for (i, s) in ["a", "b", "b", "b", "c", "c"].iter().enumerate() {
2446            db.put("t", &i.to_string(),
2447                serde_json::json!({"g": s, "n": i}), vec![], None, None).unwrap();
2448        }
2449        let (rows, _) = query(&db, "FROM t GROUP BY g COUNT HAVING count > 1").unwrap();
2450        let mut keys: Vec<&str> = rows.iter().map(|r| r["g"].as_str().unwrap()).collect();
2451        keys.sort();
2452        assert_eq!(keys, vec!["b", "c"], "the single-row group `a` is filtered out");
2453    }
2454
2455    #[test]
2456    fn having_filters_on_the_aggregate_value() {
2457        let (_tmp, db) = setup_items();
2458        // x sums to 15, y to 60.
2459        let (rows, _) = query(
2460            &db, "FROM items GROUP BY cat SUM price HAVING sum_price > 20").unwrap();
2461        assert_eq!(rows.len(), 1);
2462        assert_eq!(rows[0]["cat"], "y");
2463    }
2464
2465    /// HAVING gets the full predicate surface, because it runs through the
2466    /// same evaluator as WHERE rather than a second copy.
2467    #[test]
2468    fn having_supports_the_whole_predicate_surface() {
2469        let (_tmp, db) = setup_items();
2470        let (in_, _) = query(
2471            &db, r#"FROM items GROUP BY cat COUNT HAVING cat IN ("x")"#).unwrap();
2472        assert_eq!(in_.len(), 1);
2473        assert_eq!(in_[0]["cat"], "x");
2474
2475        let (btw, _) = query(
2476            &db, "FROM items GROUP BY cat SUM price HAVING sum_price BETWEEN 10 AND 20").unwrap();
2477        assert_eq!(btw.len(), 1);
2478        assert_eq!(btw[0]["cat"], "x");
2479
2480        let (like, _) = query(
2481            &db, r#"FROM items GROUP BY cat COUNT HAVING cat LIKE "y""#).unwrap();
2482        assert_eq!(like.len(), 1);
2483
2484        let (or_, _) = query(
2485            &db, "FROM items GROUP BY cat SUM price HAVING sum_price < 20 OR count = 3").unwrap();
2486        assert_eq!(or_.len(), 2);
2487    }
2488
2489    /// WHERE filters input rows, HAVING filters groups. Confusing them gives
2490    /// different answers, so the distinction must hold.
2491    #[test]
2492    fn where_and_having_are_different_stages() {
2493        let (_tmp, db) = setup_items();
2494        // WHERE drops rows BEFORE grouping, shrinking the sums.
2495        let (w, _) = query(
2496            &db, "FROM items WHERE price > 10 GROUP BY cat SUM price").unwrap();
2497        let x = w.iter().find(|r| r["cat"] == "x");
2498        assert!(x.is_none(), "x's rows (0,5,10) are all filtered out by WHERE");
2499
2500        // HAVING keeps every row in the aggregate and filters the RESULT.
2501        let (h, _) = query(
2502            &db, "FROM items GROUP BY cat SUM price HAVING sum_price > 10").unwrap();
2503        assert_eq!(h.len(), 2, "both groups sum above 10 when nothing is pre-filtered");
2504    }
2505
2506    #[test]
2507    fn having_without_an_aggregate_is_an_error() {
2508        let (_tmp, db) = setup();
2509        // HAVING is meaningless without grouping, and silently treating it as
2510        // a second WHERE would be exactly the kind of reinterpretation this
2511        // parser no longer does.
2512        assert!(query(&db, "FROM blocks HAVING height > 3").is_err());
2513    }
2514
2515    // ── Bare aggregates, no GROUP BY ────────────────────────────────────────
2516
2517    /// `FROM t COUNT` — "how many rows match?" without fetching them. The
2518    /// July engine note recorded `SELECT COUNT(*)` returning `[]` silently;
2519    /// this is the capability that was missing behind that silence.
2520    #[test]
2521    fn bare_count_returns_one_row() {
2522        let (_tmp, db) = setup();
2523        let (rows, _) = query(&db, "FROM blocks COUNT").unwrap();
2524        assert_eq!(rows.len(), 1);
2525        assert_eq!(rows[0]["count"], json!(5));
2526        assert_eq!(rows[0]["value"], json!(5));
2527    }
2528
2529    #[test]
2530    fn bare_count_respects_the_filter() {
2531        let (_tmp, db) = setup();
2532        let (rows, _) = query(&db, "FROM blocks WHERE height > 3 COUNT").unwrap();
2533        assert_eq!(rows[0]["count"], json!(2));
2534    }
2535
2536    #[test]
2537    fn bare_sum_avg_min_max() {
2538        let (_tmp, db) = setup();
2539        // heights 1..=5, n_tx = h*2 -> 2,4,6,8,10
2540        let (s, _) = query(&db, "FROM blocks SUM n_tx").unwrap();
2541        assert_eq!(s[0]["sum_n_tx"], json!(30), "integer inputs give an integer sum");
2542        let (a, _) = query(&db, "FROM blocks AVG n_tx").unwrap();
2543        assert_eq!(a[0]["avg_n_tx"], json!(6.0));
2544        let (mn, _) = query(&db, "FROM blocks MIN n_tx").unwrap();
2545        assert_eq!(mn[0]["min_n_tx"], json!(2));
2546        let (mx, _) = query(&db, "FROM blocks MAX n_tx").unwrap();
2547        assert_eq!(mx[0]["max_n_tx"], json!(10));
2548    }
2549
2550    /// Integer inputs must produce integer aggregates.
2551    ///
2552    /// Aggregating exclusively in f64 was a type divergence from the Python
2553    /// reference (which returns `66`, not `66.0`) AND a precision bug: f64
2554    /// cannot represent integers above 2^53 exactly, so a SUM over satoshi
2555    /// amounts or block heights silently rounded. This engine stores exactly
2556    /// that kind of number.
2557    #[test]
2558    fn integer_aggregates_stay_integers_and_keep_full_precision() {
2559        let dir = tempdir().unwrap();
2560        let db = Db::open(dir.path(), None).unwrap();
2561        // Beyond 2^53 (9_007_199_254_740_992), where f64 starts skipping
2562        // integers. Their true sum ends in ...9, which a f64 round-trip loses.
2563        let big: [i64; 3] = [9_007_199_254_740_993, 9_007_199_254_740_995, 1];
2564        for (i, v) in big.iter().enumerate() {
2565            db.put("t", &i.to_string(), serde_json::json!({"v": v}),
2566                   vec![], None, None).unwrap();
2567        }
2568        let (s, _) = query(&db, "FROM t SUM v").unwrap();
2569        assert_eq!(s[0]["sum_v"], json!(18_014_398_509_481_989i64),
2570                   "exact i64 sum, not a rounded f64");
2571        assert!(s[0]["sum_v"].is_i64(), "must serialise as an integer");
2572
2573        let (mx, _) = query(&db, "FROM t MAX v").unwrap();
2574        assert_eq!(mx[0]["max_v"], json!(9_007_199_254_740_995i64));
2575        let (mn, _) = query(&db, "FROM t MIN v").unwrap();
2576        assert_eq!(mn[0]["min_v"], json!(1));
2577    }
2578
2579    /// A float anywhere in the column makes the whole aggregate fractional,
2580    /// which is what Python's arithmetic does too.
2581    #[test]
2582    fn a_single_float_makes_the_aggregate_fractional() {
2583        let dir = tempdir().unwrap();
2584        let db = Db::open(dir.path(), None).unwrap();
2585        db.put("t", "1", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
2586        db.put("t", "2", serde_json::json!({"v": 2.5}), vec![], None, None).unwrap();
2587        let (s, _) = query(&db, "FROM t SUM v").unwrap();
2588        assert_eq!(s[0]["sum_v"], json!(3.5));
2589        // AVG is true division, so it is fractional even over pure integers.
2590        let (a, _) = query(&db, "FROM t AVG v").unwrap();
2591        assert_eq!(a[0]["avg_v"], json!(1.75));
2592    }
2593
2594    /// A JSON bool is not a number, matching Python's explicit
2595    /// `not isinstance(x, bool)` guard.
2596    #[test]
2597    fn booleans_are_not_aggregated_as_numbers() {
2598        let dir = tempdir().unwrap();
2599        let db = Db::open(dir.path(), None).unwrap();
2600        db.put("t", "1", serde_json::json!({"v": true}), vec![], None, None).unwrap();
2601        db.put("t", "2", serde_json::json!({"v": 5}), vec![], None, None).unwrap();
2602        let (s, _) = query(&db, "FROM t SUM v").unwrap();
2603        assert_eq!(s[0]["sum_v"], json!(5), "the bool contributes nothing");
2604        assert_eq!(s[0]["count"], json!(2), "but it still counts toward the group");
2605    }
2606
2607    /// COUNT over an empty result is 0, not "no rows". A caller asking "how
2608    /// many?" must get a number.
2609    #[test]
2610    fn bare_count_of_nothing_is_zero_not_empty() {
2611        let (_tmp, db) = setup();
2612        let (rows, count) = query(&db, "FROM blocks WHERE height > 999 COUNT").unwrap();
2613        assert_eq!(count, 1, "still exactly one row");
2614        assert_eq!(rows[0]["count"], json!(0));
2615
2616        // A GROUPED aggregate over zero rows correctly has no groups.
2617        let (g, _) = query(&db, "FROM blocks WHERE height > 999 GROUP BY height COUNT").unwrap();
2618        assert!(g.is_empty());
2619    }
2620
2621    #[test]
2622    fn bare_aggregate_over_an_empty_collection() {
2623        let (_tmp, db) = setup();
2624        let (rows, _) = query(&db, "FROM nonexistent COUNT").unwrap();
2625        assert_eq!(rows[0]["count"], json!(0));
2626        let (s, _) = query(&db, "FROM nonexistent SUM n_tx").unwrap();
2627        assert_eq!(s[0]["sum_n_tx"], Value::Null, "sum of nothing is null, not 0");
2628    }
2629
2630    #[test]
2631    fn bare_aggregate_carries_no_group_key() {
2632        let (_tmp, db) = setup();
2633        let (rows, _) = query(&db, "FROM blocks COUNT").unwrap();
2634        if let Value::Object(m) = &rows[0] {
2635            let mut keys: Vec<&String> = m.keys().collect();
2636            keys.sort();
2637            assert_eq!(keys, vec!["count", "value"]);
2638        } else {
2639            panic!("expected an object");
2640        }
2641    }
2642
2643    /// A document field whose name collides with a reserved word must be
2644    /// addressable. The lexer uppercases keywords for matching, and field
2645    /// positions accept a keyword as a field name — but they used the
2646    /// UPPERCASED text, so `WHERE count > 1` searched the document for "COUNT"
2647    /// and matched nothing. Silent, and it hit real field names: count, min,
2648    /// max, sum, avg, value, search, group, order, limit, offset, trace.
2649    #[test]
2650    fn a_field_named_like_a_keyword_is_still_addressable() {
2651        let dir = tempdir().unwrap();
2652        let db = Db::open(dir.path(), None).unwrap();
2653        db.put("t", "1", serde_json::json!({
2654            "count": 5, "min": 1, "max": 9, "sum": 3, "avg": 2,
2655            "value": "keep", "limit": 7, "offset": 8, "group": "g1", "search": "s",
2656        }), vec![], None, None).unwrap();
2657        db.put("t", "2", serde_json::json!({
2658            "count": 1, "min": 0, "max": 2, "sum": 0, "avg": 0,
2659            "value": "drop", "limit": 0, "offset": 0, "group": "g2", "search": "t",
2660        }), vec![], None, None).unwrap();
2661
2662        for (nql, want) in [
2663            ("FROM t WHERE count > 3", "1"),
2664            ("FROM t WHERE min = 1", "1"),
2665            ("FROM t WHERE max >= 9", "1"),
2666            ("FROM t WHERE sum = 3", "1"),
2667            ("FROM t WHERE avg = 2", "1"),
2668            (r#"FROM t WHERE value = "keep""#, "1"),
2669            ("FROM t WHERE limit = 7", "1"),
2670            ("FROM t WHERE offset = 8", "1"),
2671            (r#"FROM t WHERE group = "g1""#, "1"),
2672        ] {
2673            let (rows, _) = query(&db, nql).unwrap();
2674            assert_eq!(rows.len(), 1, "`{}` matched {} rows", nql, rows.len());
2675            assert_eq!(rows[0]["_id"], want, "`{}`", nql);
2676        }
2677
2678        // Sorting and grouping on such a field too.
2679        let (ord, _) = query(&db, "FROM t ORDER BY count DESC").unwrap();
2680        assert_eq!(ord[0]["_id"], "1");
2681        let (grp, _) = query(&db, "FROM t GROUP BY group COUNT").unwrap();
2682        assert_eq!(grp.len(), 2);
2683        let keys: Vec<&str> = grp.iter().filter_map(|r| r["group"].as_str()).collect();
2684        assert!(keys.contains(&"g1") && keys.contains(&"g2"), "{:?}", grp);
2685    }
2686
2687    /// The raw spelling is preserved, so a mixed-case field name round-trips
2688    /// while the keyword it collides with still matches case-insensitively.
2689    #[test]
2690    fn keyword_matching_stays_case_insensitive() {
2691        let dir = tempdir().unwrap();
2692        let db = Db::open(dir.path(), None).unwrap();
2693        db.put("t", "1", serde_json::json!({"Count": 5, "n": 1}),
2694               vec![], None, None).unwrap();
2695        // Field spelled `Count`, clause keywords in lower case.
2696        let (rows, _) = query(&db, "from t where Count = 5 order by n").unwrap();
2697        assert_eq!(rows.len(), 1);
2698        // And a differently-cased field name does NOT collide with it.
2699        let (miss, _) = query(&db, "FROM t WHERE count = 5").unwrap();
2700        assert!(miss.is_empty(), "`count` and `Count` are distinct field names");
2701    }
2702
2703    #[test]
2704    fn two_aggregates_is_an_error() {
2705        let (_tmp, db) = setup();
2706        assert!(query(&db, "FROM blocks COUNT SUM n_tx").is_err());
2707        assert!(query(&db, "FROM blocks GROUP BY height COUNT SUM n_tx").is_err());
2708    }
2709
2710    #[test]
2711    fn bare_aggregate_with_having() {
2712        let (_tmp, db) = setup();
2713        let (keep, _) = query(&db, "FROM blocks COUNT HAVING count > 3").unwrap();
2714        assert_eq!(keep.len(), 1);
2715        let (drop, _) = query(&db, "FROM blocks COUNT HAVING count > 99").unwrap();
2716        assert!(drop.is_empty());
2717    }
2718
2719    // ── GROUP BY parity with the Python reference (query.py + engine.py) ────
2720
2721    /// Fixture mirroring tests/test_v050.py::test_group_by_min_max exactly:
2722    /// six items, cat x for 0..2 and y for 3..5, price = i * 5.
2723    fn setup_items() -> (tempfile::TempDir, Db) {
2724        let dir = tempdir().unwrap();
2725        let db = Db::open(dir.path(), None).unwrap();
2726        for i in 0..6 {
2727            db.put("items", &i.to_string(),
2728                serde_json::json!({"cat": if i < 3 {"x"} else {"y"}, "price": i * 5}),
2729                vec![], None, None).unwrap();
2730        }
2731        (dir, db)
2732    }
2733
2734    fn group(rows: &[Value], field: &str, key: &str) -> Value {
2735        rows.iter()
2736            .find(|r| r[field] == Value::String(key.to_string()))
2737            .unwrap_or_else(|| panic!("no group {:?} in {:?}", key, rows))
2738            .clone()
2739    }
2740
2741    /// The aggregate must read the TARGET field. Before 3.3.0 the executor
2742    /// aggregated the GROUP BY field itself and the target was silently
2743    /// dropped by the unknown-token skip, so `MAX price` returned the max of
2744    /// `cat` — a non-numeric value coerced to 1.0, making every group answer
2745    /// 1. Python returns x:0 and y:25 for MIN/MAX respectively.
2746    #[test]
2747    fn group_by_aggregates_the_target_field_not_the_group_field() {
2748        let (_tmp, db) = setup_items();
2749
2750        let (mins, _) = query(&db, "FROM items GROUP BY cat MIN price").unwrap();
2751        assert_eq!(group(&mins, "cat", "x")["min_price"], json!(0));
2752        assert_eq!(group(&mins, "cat", "y")["min_price"], json!(15));
2753
2754        let (maxs, _) = query(&db, "FROM items GROUP BY cat MAX price").unwrap();
2755        assert_eq!(group(&maxs, "cat", "y")["max_price"], json!(25));
2756        assert_eq!(group(&maxs, "cat", "x")["max_price"], json!(10));
2757
2758        let (sums, _) = query(&db, "FROM items GROUP BY cat SUM price").unwrap();
2759        assert_eq!(group(&sums, "cat", "x")["sum_price"], json!(15));  // 0+5+10
2760        assert_eq!(group(&sums, "cat", "y")["sum_price"], json!(60));  // 15+20+25
2761
2762        let (avgs, _) = query(&db, "FROM items GROUP BY cat AVG price").unwrap();
2763        assert_eq!(group(&avgs, "cat", "x")["avg_price"], json!(5.0));
2764        assert_eq!(group(&avgs, "cat", "y")["avg_price"], json!(20.0));
2765    }
2766
2767    /// An aggregate over a `_`-prefixed metadata field must see it.
2768    ///
2769    /// `_seq` lives on the node, not in its data payload, and the aggregator
2770    /// read the payload directly — so `MAX _seq` answered NULL while
2771    /// `SELECT _seq` listed the values and `WHERE _seq > 5` filtered on them.
2772    /// It was also a live divergence: the Python engine builds its groups from
2773    /// projected dicts that already carry `_seq`, and answers correctly.
2774    ///
2775    /// "What is the newest sequence?" is the question replication and time
2776    /// travel are built on, so a confident null there is the worst shape of
2777    /// wrong answer this engine can give.
2778    #[test]
2779    fn aggregates_see_node_metadata_not_only_the_payload() {
2780        let (_tmp, db) = setup_items();
2781        let (all, _) = query(&db, "FROM items").unwrap();
2782        let want = all.iter().filter_map(|r| r.get("_seq")?.as_i64()).max().unwrap();
2783
2784        let (rows, _) = query(&db, "FROM items MAX _seq").unwrap();
2785        assert_eq!(rows[0]["max__seq"], json!(want),
2786                   "MAX _seq must equal the highest sequence in the result");
2787        assert_ne!(rows[0]["max__seq"], Value::Null, "a null here is a silent wrong answer");
2788
2789        let (rows, _) = query(&db, "FROM items MIN _seq").unwrap();
2790        assert_eq!(rows[0]["min__seq"], json!(
2791            all.iter().filter_map(|r| r.get("_seq")?.as_i64()).min().unwrap()));
2792
2793        // Grouping by a metadata field works through the same resolver.
2794        let (rows, _) = query(&db, "FROM items GROUP BY _seq COUNT").unwrap();
2795        assert_eq!(rows.len(), all.len(), "one group per distinct sequence");
2796        assert!(rows.iter().all(|r| r["_seq"] != Value::Null),
2797                "the group key must be the sequence, not null");
2798    }
2799
2800    /// Output key parity: Python's engine.py emits `<agg>_<field>` and a
2801    /// `count`. This engine additionally keeps `value` as the alias it has
2802    /// always emitted, so existing callers keep working.
2803    #[test]
2804    fn group_by_emits_python_parity_keys_and_the_value_alias() {
2805        let (_tmp, db) = setup_items();
2806        let (rows, _) = query(&db, "FROM items GROUP BY cat SUM price").unwrap();
2807        let x = group(&rows, "cat", "x");
2808        assert_eq!(x["sum_price"], json!(15), "python-parity key");
2809        assert_eq!(x["value"], json!(15), "back-compat alias must agree");
2810        assert_eq!(x["count"], json!(3), "count is the group size");
2811    }
2812
2813    /// `count` is the group size; the aggregate only sees numeric targets.
2814    /// A group of 3 where one row has a non-numeric price must still report
2815    /// count=3 while averaging over 2 — matching Python's isinstance filter.
2816    #[test]
2817    fn count_is_group_size_while_aggregate_skips_non_numeric() {
2818        let dir = tempdir().unwrap();
2819        let db = Db::open(dir.path(), None).unwrap();
2820        db.put("t", "1", serde_json::json!({"g": "a", "n": 10}), vec![], None, None).unwrap();
2821        db.put("t", "2", serde_json::json!({"g": "a", "n": 20}), vec![], None, None).unwrap();
2822        db.put("t", "3", serde_json::json!({"g": "a", "n": "N/A"}), vec![], None, None).unwrap();
2823
2824        let (rows, _) = query(&db, "FROM t GROUP BY g AVG n").unwrap();
2825        let a = group(&rows, "g", "a");
2826        assert_eq!(a["count"], json!(3), "every row counts toward the group");
2827        assert_eq!(a["avg_n"], json!(15.0), "only the two numeric rows average");
2828    }
2829
2830    /// An aggregate with no numeric input is null, not 0 and not infinity.
2831    #[test]
2832    fn empty_aggregate_input_is_null() {
2833        let dir = tempdir().unwrap();
2834        let db = Db::open(dir.path(), None).unwrap();
2835        db.put("t", "1", serde_json::json!({"g": "a", "n": "x"}), vec![], None, None).unwrap();
2836        let (rows, _) = query(&db, "FROM t GROUP BY g MIN n").unwrap();
2837        assert_eq!(rows[0]["min_n"], Value::Null);
2838        assert_eq!(rows[0]["count"], json!(1));
2839    }
2840
2841    /// Python makes the aggregate keyword optional — `GROUP BY field` alone
2842    /// yields counts. Rust used to reject it as a parse error.
2843    #[test]
2844    fn bare_group_by_without_an_aggregate_counts() {
2845        let (_tmp, db) = setup_items();
2846        let (rows, _) = query(&db, "FROM items GROUP BY cat").unwrap();
2847        assert_eq!(rows.len(), 2);
2848        assert_eq!(group(&rows, "cat", "x")["count"], json!(3));
2849        assert_eq!(group(&rows, "cat", "y")["count"], json!(3));
2850    }
2851
2852    /// SUM/AVG/MIN/MAX require a target field, as in Python.
2853    #[test]
2854    fn aggregate_without_a_target_field_is_an_error() {
2855        let (_tmp, db) = setup_items();
2856        for bad in [
2857            "FROM items GROUP BY cat SUM",
2858            "FROM items GROUP BY cat AVG",
2859            "FROM items GROUP BY cat MIN",
2860        ] {
2861            assert!(query(&db, bad).is_err(), "`{}` must be rejected", bad);
2862        }
2863    }
2864
2865    /// Group output order is first-seen, so repeated runs agree. HashMap
2866    /// iteration order previously made this nondeterministic.
2867    #[test]
2868    fn group_order_is_stable_across_runs() {
2869        let (_tmp, db) = setup_items();
2870        let first = query(&db, "FROM items GROUP BY cat SUM price").unwrap().0;
2871        for _ in 0..8 {
2872            let again = query(&db, "FROM items GROUP BY cat SUM price").unwrap().0;
2873            assert_eq!(first, again);
2874        }
2875    }
2876
2877    /// GROUP BY composes with the new predicate surface.
2878    #[test]
2879    fn group_by_after_an_in_predicate() {
2880        let (_tmp, db) = setup_items();
2881        let (rows, _) = query(
2882            &db, "FROM items WHERE price IN (0, 5, 25) GROUP BY cat SUM price").unwrap();
2883        assert_eq!(group(&rows, "cat", "x")["sum_price"], json!(5));
2884        assert_eq!(group(&rows, "cat", "y")["sum_price"], json!(25));
2885    }
2886
2887    #[test]
2888    fn search() {
2889        let (_tmp, db) = setup();
2890        let (rows, _) = query(&db, r#"FROM blocks SEARCH "0003""#).unwrap();
2891        assert_eq!(rows.len(), 1);
2892    }
2893
2894    #[test]
2895    fn as_of() {
2896        let dir = tempdir().unwrap();
2897        let db = Db::open(dir.path(), None).unwrap();
2898        let v1 = db.put("docs", "x", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
2899        db.put("docs", "x", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
2900        let (rows, _) = query(&db, &format!("FROM docs AS OF {}", v1.seq)).unwrap();
2901        assert_eq!(rows[0]["v"], 1);
2902    }
2903
2904    #[test]
2905    fn valid_as_of() {
2906        let dir = tempdir().unwrap();
2907        let db = Db::open(dir.path(), None).unwrap();
2908        db.put("events", "e1", serde_json::json!({"type": "a"}), vec![],
2909               Some("2025-01-01".to_string()), Some("2025-06-01".to_string())).unwrap();
2910        db.put("events", "e2", serde_json::json!({"type": "b"}), vec![],
2911               Some("2026-01-01".to_string()), None).unwrap();
2912        let (rows, _) = query(&db, r#"FROM events VALID AS OF "2025-03-01""#).unwrap();
2913        assert_eq!(rows.len(), 1);
2914        assert_eq!(rows[0]["type"], "a");
2915    }
2916
2917    // ── String-literal escaping ──────────────────────────────────────────────
2918
2919    #[test]
2920    fn escaped_quote_matches_a_value_containing_a_quote() {
2921        let dir = tempdir().unwrap();
2922        let db = Db::open(dir.path(), None).unwrap();
2923        db.put("m", "q", serde_json::json!({ "name": "say \"hi\"" }), vec![], None, None)
2924            .unwrap();
2925        db.put("m", "p", serde_json::json!({ "name": "plain" }), vec![], None, None).unwrap();
2926
2927        // \" inside the literal is a literal quote; the string does not end there.
2928        let (rows, count) = query(&db, r#"FROM m WHERE name = "say \"hi\"""#).unwrap();
2929        assert_eq!(count, 1, "the escaped-quote literal matches exactly one row");
2930        assert_eq!(rows[0]["_id"], "q");
2931    }
2932
2933    #[test]
2934    fn raw_backslash_still_matches_literally() {
2935        // REGRESSION GUARD: a lone backslash stays literal, so pre-existing
2936        // backslash queries (Windows paths etc.) keep matching. This is the
2937        // property that makes the \" addition non-breaking.
2938        let dir = tempdir().unwrap();
2939        let db = Db::open(dir.path(), None).unwrap();
2940        db.put("m", "b", serde_json::json!({ "p": "back\\slash" }), vec![], None, None).unwrap();
2941
2942        let (rows, count) = query(&db, r#"FROM m WHERE p = "back\slash""#).unwrap();
2943        assert_eq!(count, 1, "a raw backslash literal matches as before");
2944        assert_eq!(rows[0]["_id"], "b");
2945    }
2946
2947    #[test]
2948    fn a_quote_can_no_longer_inject_trailing_clauses() {
2949        // The security motivation: previously a value of `x" LIMIT 1` would
2950        // terminate the literal and inject `LIMIT 1`. With \" the caller can
2951        // escape the quote so it stays part of the value and matches nothing
2952        // rogue. Here the escaped form matches the literal value verbatim.
2953        let dir = tempdir().unwrap();
2954        let db = Db::open(dir.path(), None).unwrap();
2955        db.put("m", "x", serde_json::json!({ "v": "a\"b" }), vec![], None, None).unwrap();
2956        let (rows, count) = query(&db, r#"FROM m WHERE v = "a\"b""#).unwrap();
2957        assert_eq!(count, 1);
2958        assert_eq!(rows[0]["_id"], "x");
2959    }
2960}
2961
2962#[cfg(test)]
2963mod tests_traverse {
2964    use super::*;
2965    use tempfile::tempdir;
2966    use crate::db::Db;
2967
2968    #[test]
2969    fn traverse_one_hop() {
2970        let db = Db::in_memory();
2971        db.put("driver", "d1", serde_json::json!({"name": "Bob"}),   vec![], None, None).unwrap();
2972        db.put("driver", "d2", serde_json::json!({"name": "Carol"}), vec![], None, None).unwrap();
2973        db.put("trip",   "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
2974        db.put("trip",   "t2", serde_json::json!({"status": "ok"}),  vec![], None, None).unwrap();
2975
2976        db.link("driver:d1", "handles", "trip:t1").unwrap();
2977        db.link("driver:d1", "handles", "trip:t2").unwrap();
2978
2979        let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
2980        assert_eq!(count, 2);
2981        let ids: std::collections::HashSet<&str> = rows.iter()
2982            .filter_map(|r| r["_id"].as_str())
2983            .collect();
2984        assert!(ids.contains("t1") && ids.contains("t2"));
2985    }
2986
2987    #[test]
2988    fn traverse_returns_empty_when_no_links() {
2989        let db = Db::in_memory();
2990        db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
2991        let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
2992        assert_eq!(count, 0);
2993        assert!(rows.is_empty());
2994    }
2995
2996    #[test]
2997    fn traverse_multi_source() {
2998        // When WHERE matches multiple rows, TRAVERSE unions all their neighbors
2999        let db = Db::in_memory();
3000        db.put("driver", "d1", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
3001        db.put("driver", "d2", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
3002        db.put("trip",   "t1", serde_json::json!({"n": 1}), vec![], None, None).unwrap();
3003        db.put("trip",   "t2", serde_json::json!({"n": 2}), vec![], None, None).unwrap();
3004        db.put("trip",   "t3", serde_json::json!({"n": 3}), vec![], None, None).unwrap();
3005
3006        db.link("driver:d1", "handles", "trip:t1").unwrap();
3007        db.link("driver:d1", "handles", "trip:t2").unwrap();
3008        db.link("driver:d2", "handles", "trip:t3").unwrap();
3009
3010        let (_rows, count) = query(&db, r#"FROM driver WHERE status = "active" TRAVERSE handles"#).unwrap();
3011        assert_eq!(count, 3);
3012    }
3013
3014    #[test]
3015    fn traverse_nql_keyword_case_insensitive() {
3016        // Parser normalises to uppercase — "traverse" and "TRAVERSE" both work
3017        let db = Db::in_memory();
3018        db.put("driver", "d1", serde_json::json!({}), vec![], None, None).unwrap();
3019        db.put("trip",   "t1", serde_json::json!({}), vec![], None, None).unwrap();
3020        db.link("driver:d1", "handles", "trip:t1").unwrap();
3021        // uppercase
3022        let (r1, c1) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3023        assert_eq!(c1, 1);
3024        // lowercase (lexer uppercases keywords)
3025        let (r2, c2) = query(&db, r#"FROM driver WHERE _id = "d1" traverse handles"#).unwrap();
3026        assert_eq!(c2, 1);
3027        assert_eq!(r1[0]["_id"], r2[0]["_id"]);
3028    }
3029
3030    #[test]
3031    fn traverse_durable() {
3032        let dir = tempdir().unwrap();
3033        {
3034            let db = Db::open(dir.path(), None).unwrap();
3035            db.put("driver", "d1", serde_json::json!({"name": "Bob"}),   vec![], None, None).unwrap();
3036            db.put("trip",   "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
3037            db.link("driver:d1", "handles", "trip:t1").unwrap();
3038        }
3039        let db2 = Db::open(dir.path(), None).unwrap();
3040        db2.startup_ready.store(true, std::sync::atomic::Ordering::SeqCst);
3041        let (rows, count) = query(&db2, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3042        assert_eq!(count, 1);
3043        assert_eq!(rows[0]["_id"], "t1");
3044    }
3045}