Skip to main content

omgbase_surface/
query.rs

1//! The runner (`spec/surface/README.md` §1.4): parse the source, rewrite the
2//! row functions to `$self` methods, run it through the engine over the
3//! store context, and shape the engine's result into the surface's
4//! `OqxResult` (lean `{ id, path, … }` hits, keyset paging, the consumer
5//! scalars). Port of `packages/core/src/oqx-js/run.ts`.
6//!
7//! By default the tier-3 planner ([`crate::planner`]) pre-filters the scan in
8//! SQL and the in-memory engine finishes the residual over the produced rows;
9//! [`QueryOptions::in_memory`] forces the pure in-memory engine (the
10//! differential gate runs every case both ways — a planner must be invisible).
11
12use std::collections::HashMap;
13
14use omgbase_search::{EmbeddingProvider, f32_to_blob};
15use omgbase_store::Store;
16use oqx::ast::{BinaryOp, Expr, Flatten, Query, SelectItem};
17use oqx::walk::{Clause, Node, VisitContext, Visitor, transform, visit};
18use oqx::{Consumer, Engine, InMemoryEngine, Value, build, resolve_aliases};
19use serde_json::{Map, Value as Json};
20
21use crate::context::{SemanticVec, StoreContext, Target, render_row_values, target_of};
22use crate::cursor::{decode_path_cursor, encode_cursor};
23use crate::dates;
24use crate::error::{Result, SurfaceError};
25use crate::names::{UnknownNames, VocabularyCache, check_names};
26use crate::paths::reference_path;
27use crate::planner::{SqlitePlanner, root_target};
28
29/// The default page size.
30pub const DEFAULT_LIMIT: usize = 50;
31
32/// Row-scoped domain functions: authored as free calls that implicitly
33/// reference the current row; rewritten to `$self.fn(…)`. `refs(x)` is not
34/// one: it reads no row, so it stays a free function of the context. The
35/// planner treats every call alike (declined by the translator, flagged by
36/// `expr_may_raise`), `has_edge` and `refs` included.
37const ROW_FNS: [&str; 12] = [
38    "text",
39    "semantic",
40    "under",
41    "under_heading",
42    "within",
43    "under_kind",
44    "yaml_path",
45    "json_pointer",
46    "has_edge",
47    "has_anchor",
48    "child_count",
49    "parent_type",
50];
51
52const ID_KEY: &str = "__oqx_id";
53const PATH_KEY: &str = "__oqx_path";
54/// The hit envelope's keys (§1.4, 3.0): the row's identity under the same
55/// bare names the expression language reads, so a key copied from a hit is
56/// valid in the next `where` with no translation; `select id, path` is simply
57/// the envelope and any other projection whose output key would be one of
58/// these must be aliased ([`envelope_collision`]).
59pub(crate) const HIT_ID: &str = "id";
60pub(crate) const HIT_PATH: &str = "path";
61/// A hit is a store row (§1.4, 1.5). A bare root scan's rows are store rows by
62/// construction; a `follow` destination (`follow before` over a frontmatter
63/// list of paths), a `from E` re-projection or any other source can reach a
64/// scalar, which the injected `id`/`path` reads would render as the junk hit
65/// `{ id: "undefined", path: "" }`. For those queries the row itself is
66/// projected too (`$it`) under this key and [`to_hit`] fails the query when it
67/// is not a row. Only then: the clone of every projected row is a real cost
68/// here, and a root scan cannot need it. A `values` projection returns no
69/// hits, so it is exempt. Same rule as the reference's `SELF_ITEM`.
70const SELF_KEY: &str = "__oqx_self";
71/// The reserved key a top-level `values` projection's single item is renamed
72/// to, so it rides through id/path injection, paging and distinct as an
73/// ordinary field and is peeled off at the end.
74const VALUE_KEY: &str = "__oqx_value";
75
76/// `query`'s options.
77#[derive(Clone, Copy, Default)]
78pub struct QueryOptions<'a> {
79    /// The page cap (default 50).
80    pub limit: Option<usize>,
81    /// Resume after a truncated page's cursor.
82    pub cursor: Option<&'a str>,
83    /// The provider behind `semantic(...)`; `None` → `semantic_unavailable`
84    /// when the query names a phrase.
85    pub provider: Option<&'a dyn EmbeddingProvider>,
86    /// Force the pure in-memory engine (skip the tier-3 pushdown planner).
87    /// The default plans; the differential gate runs both and compares.
88    pub in_memory: bool,
89    /// `bare: true` (§1.4, 4.2): every hit is its projection only — the
90    /// automatic `id`/`path` envelope members are stripped when the result is
91    /// shaped, AFTER paging and distinct (identity is kept internally: the
92    /// keyset cursor and the dedup key read the envelope). Needs a `select`
93    /// (without a projection a hit is only its envelope) and is refused with
94    /// `values` (already the bare-values form); a projected `id`/`path` is the
95    /// row's intrinsic, kept as any projected key is.
96    pub bare: bool,
97    /// What a tier-3 name on an open kind does (`spec/surface` §1.5, 4.6; a
98    /// tier-3 name on `links` is `filter_invalid` either way): `Hint` records
99    /// it in `hints` and runs the query — the default of the source entry
100    /// ([`query`], every read); `Refuse` fails with `filter_invalid` before
101    /// any row is read — the default of the AST entry ([`query_ast`]), whose
102    /// caller is the write selector compiler, where a name no row carries
103    /// would select every row. `None` takes the entry's default.
104    pub unknown_names: Option<UnknownNames>,
105    /// The vocabulary cache of the store (§1.5 "Caching"), kept by the
106    /// `Surface` beside its store; `None` judges with a cache of the run's own.
107    pub vocabulary: Option<&'a VocabularyCache>,
108    /// The instant the named points of `date()` resolve against
109    /// (`spec/surface` §1.3, 4.8): the host clock, UTC, in milliseconds —
110    /// the `Surface` passes the clock it stamps commits with (pinned by
111    /// `OMGBASE_SPEC_CLOCK`); `None` reads the wall clock. One instant per
112    /// run: the fold and the context share it.
113    pub now_ms: Option<i64>,
114    /// The `query` call as the caller received it (`spec/surface` 4.11): the
115    /// arguments a corrected call is spelled from when a parse error can be
116    /// fixed (`data.fix`, the bracket-list rule — [`bracket_list_as_tuple`]).
117    /// Absent, the message still gains its hint and no `fix` is composed.
118    pub request: Option<&'a Json>,
119}
120
121/// §1.4 `OqxResult`.
122#[derive(Clone, Debug, PartialEq)]
123pub struct OqxResult {
124    pub hits: Vec<Json>,
125    pub truncated: bool,
126    pub cursor: Option<String>,
127    pub consumer: Consumer,
128    pub count: Option<f64>,
129    pub exists: Option<bool>,
130    pub none: Option<bool>,
131    /// A top-level `values` projection's bare values, in place of `hits`.
132    pub values: Option<Vec<Json>>,
133    /// Unknown names (`spec/surface` §1.5, 4.6): one line per name the query
134    /// reads that is neither a field of its row kind nor a property any object
135    /// of that kind in the repository carries — the rows are still returned.
136    /// First-reference order, deduplicated; empty = absent on the wire (never
137    /// `[]`). The last member of the result.
138    pub hints: Vec<String>,
139}
140
141impl OqxResult {
142    fn scalar(consumer: Consumer) -> Self {
143        Self {
144            hits: Vec::new(),
145            truncated: false,
146            cursor: None,
147            consumer,
148            count: None,
149            exists: None,
150            none: None,
151            values: None,
152            hints: Vec::new(),
153        }
154    }
155
156    /// The wire shape: `{ hits, truncated, cursor, consumer, count?, exists?,
157    /// none?, values?, hints? }` — `hints` last, only when there is one.
158    #[must_use]
159    pub fn to_json(&self) -> Json {
160        let mut m = Map::new();
161        m.insert("hits".to_owned(), Json::Array(self.hits.clone()));
162        m.insert("truncated".to_owned(), Json::Bool(self.truncated));
163        m.insert(
164            "cursor".to_owned(),
165            self.cursor.clone().map_or(Json::Null, Json::String),
166        );
167        m.insert(
168            "consumer".to_owned(),
169            Json::String(self.consumer.as_str().to_owned()),
170        );
171        if let Some(n) = self.count {
172            m.insert("count".to_owned(), Value::Number(n).to_canonical_json());
173        }
174        if let Some(b) = self.exists {
175            m.insert("exists".to_owned(), Json::Bool(b));
176        }
177        if let Some(b) = self.none {
178            m.insert("none".to_owned(), Json::Bool(b));
179        }
180        if let Some(v) = &self.values {
181            m.insert("values".to_owned(), Json::Array(v.clone()));
182        }
183        if !self.hints.is_empty() {
184            m.insert(
185                "hints".to_owned(),
186                Json::Array(self.hints.iter().cloned().map(Json::String).collect()),
187            );
188        }
189        Json::Object(m)
190    }
191}
192
193// ---- the `$self` rewrite -----------------------------------------------------------
194
195/// Paths on the surface are the reference form (`spec/surface` §1 "Paths",
196/// 2.0): `path` and the edge field `dst_path` read `/a/b.md`. A string
197/// literal a query compares with one of them — `path == "a.md"`, `"a.md" !=
198/// dst_path`,
199/// `path.startsWith("lab/")` — is rooted first, so both spellings match; a
200/// property or binding compared with a path is never touched (it is the
201/// author's value). The planner sees the rewritten tree too, so the pushed
202/// SQL and the in-memory engine agree by construction.
203const PATH_ATTRIBUTES: [&str; 2] = ["path", "dst_path"];
204/// The handles through which a member read reaches a ROW's `path` (`doc.path`,
205/// `block.path`, a link's `dst.path`, `$it.path`); a member read through a
206/// source handle (`data.path`, `frontmatter.path`) is the author's value and
207/// is never rooted.
208const ROW_HANDLES: [&str; 4] = ["doc", "block", "dst", "$it"];
209
210fn is_path_read(e: &Expr) -> bool {
211    match e {
212        Expr::Ident { name, .. } | Expr::Outer { name, .. } => {
213            PATH_ATTRIBUTES.contains(&name.as_str())
214        }
215        Expr::Member { recv, name, .. } if PATH_ATTRIBUTES.contains(&name.as_str()) => {
216            matches!(&**recv, Expr::Ident { name, .. } | Expr::Outer { name, .. } if ROW_HANDLES.contains(&name.as_str()))
217        }
218        _ => false,
219    }
220}
221
222/// Result projections may not overwrite the envelope (§1.4, 3.0): a projection
223/// whose output key is `id` or `path` is allowed only when it IS the envelope's
224/// own selection — the bare attribute of the same name, which collapses onto
225/// the envelope key (`select id, path` is simply the envelope) — and is
226/// otherwise a loud error asking for an alias: `select data.path` must be
227/// `select p: data.path`, `select path: doc.layer` likewise. Checked over the
228/// resolved items (an alias is just a name here), before the engine runs, so
229/// the error never depends on the rows. A `values` projection returns no hits,
230/// so the envelope — and the rule — do not apply. Same rule as the reference's
231/// `envelopeCollision`.
232fn envelope_collision(items: &[SelectItem]) -> Option<SurfaceError> {
233    for item in items {
234        let name = match item {
235            SelectItem::Field { name, .. } | SelectItem::Collect { name, .. } => name.as_str(),
236        };
237        if name != HIT_ID && name != HIT_PATH {
238            continue;
239        }
240        if let SelectItem::Field { expr, .. } = item
241            && matches!(expr, Expr::Ident { name: n, .. } if n == name)
242        {
243            continue;
244        }
245        let hint = if name == HIT_PATH {
246            "p: data.path"
247        } else {
248            "i: data.id"
249        };
250        return Some(SurfaceError::filter_invalid(
251            format!("projection `{name}` collides with the hit envelope — alias it (`{hint}`)"),
252            "OQX",
253        ));
254    }
255    None
256}
257
258/// A `flatten … as id` / `as path` (oqx 0.25) would shadow the envelope's own
259/// reads on the flattened row (the bound name shadows a same-named row
260/// property, SEMANTICS §25): refused, like a projection that collides with the
261/// envelope. Same rule as the reference's `runResolved`.
262fn flatten_collision(flatten: &[Flatten]) -> Option<SurfaceError> {
263    let f = flatten
264        .iter()
265        .find(|f| f.name == HIT_ID || f.name == HIT_PATH)?;
266    Some(SurfaceError::filter_invalid(
267        format!(
268            "flatten `as {0}` collides with the hit envelope — a flattened row is still the row, whose `{0}` the envelope reads; bind another name",
269            f.name
270        ),
271        "OQX",
272    ))
273}
274
275fn root_literal(e: Expr) -> Expr {
276    match e {
277        Expr::Lit {
278            value: Value::Str(s),
279            span,
280        } => Expr::Lit {
281            value: Value::Str(reference_path(&s)),
282            span,
283        },
284        other => other,
285    }
286}
287
288/// The literal-rooting rewrite of one expression (see [`rewrite_query`]).
289#[must_use]
290pub fn root_path_literals(e: Expr) -> Expr {
291    match e {
292        Expr::Binary {
293            op: op @ (BinaryOp::Eq | BinaryOp::Ne),
294            left,
295            right,
296            span,
297        } => {
298            if is_path_read(&left) {
299                Expr::Binary {
300                    op,
301                    left,
302                    right: Box::new(root_literal(*right)),
303                    span,
304                }
305            } else if is_path_read(&right) {
306                Expr::Binary {
307                    op,
308                    left: Box::new(root_literal(*left)),
309                    right,
310                    span,
311                }
312            } else {
313                Expr::Binary {
314                    op,
315                    left,
316                    right,
317                    span,
318                }
319            }
320        }
321        Expr::Call {
322            recv: Some(recv),
323            name,
324            mut args,
325            span,
326        } if name == "startsWith" && args.len() == 1 && is_path_read(&recv) => {
327            let arg = root_literal(args.remove(0));
328            Expr::Call {
329                recv: Some(recv),
330                name,
331                args: vec![arg],
332                span,
333            }
334        }
335        other => other,
336    }
337}
338
339/// Dates over literals fold to literals (`spec/surface` §1.3 "Dates and
340/// durations", 4.8): `date("today")`, `dur("30 days")`, and a date method
341/// whose receiver and arguments are literals (`date("today").minus(dur("30
342/// days"))`, folded inside-out since `transform` maps children first) become
343/// the string / number / null they evaluate to — by the SAME functions the
344/// context runs, so nothing changes in meaning — before the planner sees the
345/// tree. So `where updated_at < date("today")` is a text-vs-text comparison
346/// the translator pushes down (a call is otherwise an unconditional decline),
347/// and a named point is resolved ONCE per query, at `now_ms`. A call with the
348/// wrong arity is left in place: it raises when evaluated, and an empty source
349/// never evaluates it (`spec/oqx` §11).
350#[must_use]
351pub fn fold_dates(e: Expr, now_ms: i64) -> Expr {
352    let Expr::Call {
353        recv, name, args, ..
354    } = &e
355    else {
356        return e;
357    };
358    let mut vals = Vec::with_capacity(args.len());
359    for a in args {
360        let Expr::Lit { value, .. } = a else { return e };
361        vals.push(value.clone());
362    }
363    let folded = match recv {
364        None => {
365            if dates::date_function_arity(name) != Some(vals.len()) {
366                return e;
367            }
368            dates::date_function(name, &vals, now_ms)
369        }
370        Some(r) => {
371            let Expr::Lit { value, .. } = &**r else {
372                return e;
373            };
374            if dates::date_method_arity(name) != Some(vals.len()) {
375                return e;
376            }
377            dates::date_method(name, value, &vals)
378        }
379    };
380    match folded {
381        Some(Ok(v)) => build::lit(v),
382        _ => e,
383    }
384}
385
386/// Rewrite every row function in a parsed query to a `$self` method call,
387/// root every string literal compared with a path intrinsic, and fold the
388/// dates over literals ([`fold_dates`], at `now_ms`) — one `transform` over
389/// the AST (`spec/oqx/AST.md` §5), every block included.
390#[must_use]
391pub fn rewrite_query(q: &Query, now_ms: i64) -> Query {
392    transform(q, &mut |e, _| match e {
393        Expr::Call {
394            recv: None,
395            name,
396            args,
397            span,
398        } if ROW_FNS.contains(&name.as_str()) => Expr::Call {
399            recv: Some(Box::new(build::ident("$self"))),
400            name,
401            args,
402            span,
403        },
404        other => fold_dates(root_path_literals(other), now_ms),
405    })
406}
407
408// ---- the root row (spec/surface §1.1, 2.0) ----------------------------------------------
409
410/// The `$repo` error (removed in surface 2.0): the same text in both engines.
411pub const REPO_REMOVED_MESSAGE: &str = "`$repo` was removed in surface 2.0 — reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^id` / `0^id`";
412
413/// The bare-target error. `depth` is the scope depth the name was read at when
414/// known statically (the carets in the hint count it); `None` when raised at
415/// read time by the context, where the hint spells out the rule instead.
416#[must_use]
417pub fn bare_target_message(t: Target, depth: Option<usize>) -> String {
418    let name = t.as_str();
419    let noun = t.noun();
420    let carets = "^".repeat(depth.unwrap_or(1));
421    let how = if depth.is_none() {
422        format!("; one caret per enclosing block, or the absolute `0^{name}`")
423    } else {
424        String::new()
425    };
426    format!(
427        "`{name}` inside a block reads a property of the current row, which has none — did you mean `{carets}{name}` (the repository's {noun}{how})?"
428    )
429}
430
431/// The unknown-target error (3.0 name pass): a top-level `from <name>` whose
432/// name is not one of the five collections would read an absent root and
433/// return an empty result — refused instead, naming the collections (in the
434/// root object's order, `revisions` the fifth since 4.7); `edges`, the links
435/// target's name before 3.0, gets its new name spelled out.
436#[must_use]
437pub fn unknown_target_message(name: &str) -> String {
438    let was = if name == "edges" {
439        " (`edges` was renamed `links` in surface 3.0)"
440    } else {
441        ""
442    };
443    let roots: Vec<String> = crate::context::ALL_TARGETS
444        .iter()
445        .map(|t| format!("`{}`", t.as_str()))
446        .collect();
447    format!(
448        "`{name}` is not a target — the repository's collections are {}{was}",
449        roots.join(", ")
450    )
451}
452
453/// The past-the-root error: `^docs` at the root scope (more carets than there
454/// are enclosing scopes) would read absent and count 0 — refused, naming the
455/// bare spelling. `levels` is the caret count written.
456#[must_use]
457pub fn past_root_message(t: Target, levels: usize) -> String {
458    let name = t.as_str();
459    let noun = t.noun();
460    let carets = "^".repeat(levels);
461    format!(
462        "`{carets}{name}` reaches past the root — there is no enclosing row at this depth; at the top level the repository's {noun} are the bare `{name}` (`{name} count {{ … }}`, `from {name}`, `entries({name})`)"
463    )
464}
465
466/// The repository is OQX's root row (`spec/oqx` 0.18): from a top-level row
467/// `^docs` reaches the documents (one caret per enclosing block, or the
468/// absolute `0^docs`) and `^id` the repository id. Two spellings are refused
469/// before evaluation, by one walk over the parsed tree, so the error does not
470/// depend on which rows the data holds and the planned and in-memory paths
471/// agree trivially: `$repo` anywhere (bare, as a member head, `^$repo`,
472/// `0^$repo`), and a bare `docs`/`links` at scope depth ≥ 1 — a property read
473/// of a row that has none. `blocks`/`nodes` are relations of some rows
474/// (`docs.blocks`, `docs.nodes`, `blocks.nodes`, `section.blocks`) and are
475/// judged by the context at read time with the same message. A `^<target>`
476/// with more carets than enclosing scopes (`^docs count { }` at the top level)
477/// would read absent and count 0: refused too, naming the bare spelling. Port
478/// of the reference's `checkRootSpellings`.
479pub fn check_root_spellings(q: &Query) -> Result<()> {
480    // A top-level `from <name>` that is no collection (`from edges`, the links
481    // target's name before 3.0) would scan an absent root and come back empty:
482    // refused, naming the five collections.
483    if let Expr::Ident { name, .. } = &q.source {
484        if Target::parse(name).is_none() {
485            return Err(SurfaceError::filter_invalid(
486                unknown_target_message(name),
487                "OQX",
488            ));
489        }
490    }
491    struct Check(Option<SurfaceError>);
492    impl Visitor<'_> for Check {
493        fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
494            if self.0.is_some() {
495                return false;
496            }
497            match node {
498                Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. })
499                    if name == "$repo" =>
500                {
501                    self.0 = Some(SurfaceError::filter_invalid(REPO_REMOVED_MESSAGE, "OQX"));
502                }
503                Node::Expr(Expr::Ident { name, .. })
504                    if ctx.depth >= 1 && matches!(name.as_str(), "docs" | "links") =>
505                {
506                    let t = Target::parse(name).expect("a target name");
507                    self.0 = Some(SurfaceError::filter_invalid(
508                        bare_target_message(t, Some(ctx.depth)),
509                        "OQX",
510                    ));
511                }
512                Node::Expr(Expr::Outer { name, levels, .. }) if *levels > ctx.depth => {
513                    if let Some(t) = Target::parse(name) {
514                        self.0 = Some(SurfaceError::filter_invalid(
515                            past_root_message(t, *levels),
516                            "OQX",
517                        ));
518                    }
519                }
520                _ => {}
521            }
522            self.0.is_none()
523        }
524    }
525    let mut check = Check(None);
526    visit(Node::Query(q), &mut check);
527    check.0.map_or(Ok(()), Err)
528}
529
530// ---- semantic phrases -----------------------------------------------------------------
531
532/// Collects the distinct `semantic("…")` phrases of a tree.
533struct Phrases(Vec<String>);
534
535impl Visitor<'_> for Phrases {
536    fn enter(&mut self, node: Node<'_>, _ctx: &VisitContext<'_>) -> bool {
537        if let Node::Expr(Expr::Call {
538            recv: None,
539            name,
540            args,
541            ..
542        }) = node
543            && name == "semantic"
544            && let Some(Expr::Lit {
545                value: Value::Str(s),
546                ..
547            }) = args.first()
548            && !self.0.contains(s)
549        {
550            self.0.push(s.clone());
551        }
552        true
553    }
554}
555
556/// The distinct phrases `semantic("…")` names (free calls in the raw parse);
557/// empty when the source does not parse.
558#[must_use]
559pub fn collect_semantic_phrases(source: &str) -> Vec<String> {
560    let Ok(q) = oqx::parse_string(source) else {
561        return Vec::new();
562    };
563    semantic_phrases_of(&q)
564}
565
566/// [`collect_semantic_phrases`] over a parsed tree.
567#[must_use]
568pub fn semantic_phrases_of(q: &Query) -> Vec<String> {
569    let mut phrases = Phrases(Vec::new());
570    visit(Node::Query(q), &mut phrases);
571    phrases.0
572}
573
574/// Embed the query's phrases through the provider: the vectors
575/// `semantic(...)` ranks by. Without a provider the phrases stay unembedded
576/// and the context reports `filter_invalid` ("needs an embedding provider")
577/// when one is reached — after its target check, so `semantic()` on nodes
578/// names the targets (§9; the `query` tool's pre-check is what reports
579/// `semantic_unavailable`).
580pub fn embed_phrases(
581    provider: Option<&dyn EmbeddingProvider>,
582    phrases: Vec<String>,
583) -> Result<HashMap<String, SemanticVec>> {
584    let mut semantic: HashMap<String, SemanticVec> = HashMap::new();
585    if let Some(provider) = provider.filter(|_| !phrases.is_empty()) {
586        for phrase in phrases {
587            let vec = provider
588                .embed_query(&phrase)
589                .map_err(|e| SurfaceError::new(e.code(), e.to_string()))?;
590            semantic.insert(
591                phrase,
592                SemanticVec {
593                    model: provider.model().to_owned(),
594                    vec: f32_to_blob(&vec),
595                },
596            );
597        }
598    }
599    Ok(semantic)
600}
601
602// ---- reserved projection names ----------------------------------------------------------
603
604/// The runner's own projection names (`__oqx_id`, `__oqx_path`,
605/// `__oqx_self`, `__oqx_value`) and the selector compiler's slots
606/// (`__omg_sel_<t>_<slot>`, [`crate::select`]) share two reserved prefixes.
607/// A user alias under either would shadow an injected item (`select
608/// __oqx_id: 1` overwrote the hit's `id` through 4.0) or collide with a
609/// slot, so it is refused before anything runs (4.1) — over the query's own
610/// top-level items, the only ones the runner injects beside. The reference's
611/// `reservedAlias`.
612pub const RESERVED_ALIAS_PREFIXES: [&str; 2] = ["__oqx_", "__omg_"];
613
614/// Refuse a top-level `select` alias under a reserved prefix.
615pub fn check_reserved_aliases(q: &Query) -> Result<()> {
616    for item in &q.select {
617        if let Some(prefix) = RESERVED_ALIAS_PREFIXES
618            .iter()
619            .find(|p| item.name().starts_with(*p))
620        {
621            return Err(SurfaceError::filter_invalid(
622                format!(
623                    "projection `{}` uses the reserved prefix `{prefix}` — choose another alias",
624                    item.name()
625                ),
626                "OQX",
627            ));
628        }
629    }
630    Ok(())
631}
632
633// ---- name mentions --------------------------------------------------------------------
634
635/// Whether `name` is read anywhere in `q` other than as its source: a bare
636/// identifier or a `^`-escaped one in the `from` steps, `where`, `select`,
637/// `order by`, `follow`, `limit`/`offset`, or any nested block.
638fn mentions_outside_source(q: &Query, name: &str) -> bool {
639    struct Mentions<'n> {
640        name: &'n str,
641        found: bool,
642    }
643    impl Visitor<'_> for Mentions<'_> {
644        fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
645            if self.found {
646                return false;
647            }
648            // The source itself (the root's `source` slot) is not a mention.
649            if ctx.clause == Some(Clause::Source) && ctx.path.len() == 1 {
650                return false;
651            }
652            if let Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. }) = node
653                && name == self.name
654            {
655                self.found = true;
656            }
657            !self.found
658        }
659    }
660    let mut m = Mentions { name, found: false };
661    visit(Node::Query(q), &mut m);
662    m.found
663}
664
665// ---- hits ----------------------------------------------------------------------------
666
667/// Whether the engine's top-level rows can be anything but store rows: a
668/// `follow` (a destination may be a property's value), a `from E`
669/// re-projection, or a source that is not a bare root scan (one of the five
670/// collections, `revisions` included — the planner's `compile` is narrower:
671/// the four it can push down).
672fn may_reach_non_rows(q: &Query) -> bool {
673    q.follow.is_some() || !q.from.is_empty() || root_target(&q.source).is_none()
674}
675
676/// The reference's `describeValue`: what a non-row hit was, for the error.
677fn describe_value(v: &Value) -> String {
678    match v {
679        Value::Undefined | Value::Null => "an absent value".to_owned(),
680        Value::Str(s) => format!(
681            "a string ({})",
682            serde_json::to_string(s).unwrap_or_default()
683        ),
684        Value::Number(_) => format!("a number ({v})"),
685        Value::Bool(b) => format!("a boolean ({b})"),
686        Value::Array(_) => "an array".to_owned(),
687        Value::Range(_) => "a range".to_owned(),
688        Value::Object(_) => "an object".to_owned(),
689    }
690}
691
692fn not_a_store_row(v: &Value) -> SurfaceError {
693    SurfaceError::filter_invalid(
694        format!(
695            "a hit must be a document, block, node, link or revision row — the query reached {}; to follow document references held in a property use refs(<field>)",
696            describe_value(v)
697        ),
698        "OQX",
699    )
700}
701
702/// A shaped row (the reference's `Shaped`, 4.2): the identity the runner
703/// injected, kept APART from the user projection until the very end, so
704/// `distinct`, the keyset page and the cursor run on identity whether or not
705/// the hits carry it (`bare`). `id` is JavaScript's `String()` on the injected
706/// id, `path` `""` for an absent path; `proj` is the user projection with its
707/// values rendered per §1.4 "rows as values" (a store row nested in the result
708/// — an empty-projection `collect { }` and friends — becomes `{ id, path }`).
709/// When the row itself was projected under [`SELF_KEY`] and is not a store
710/// row, the query fails (§1.4: a hit is a store row).
711#[derive(Debug)]
712struct Shaped {
713    id: String,
714    path: String,
715    proj: oqx::Object,
716}
717
718fn path_string(path: &Value) -> String {
719    if path.is_absent() {
720        String::new()
721    } else {
722        path.to_string()
723    }
724}
725
726fn shape_row(row: Value) -> Result<Shaped> {
727    let mut shaped = Shaped {
728        id: Value::Undefined.to_string(),
729        path: String::new(),
730        proj: oqx::Object::new(),
731    };
732    let Value::Object(mut o) = row else {
733        return Ok(shaped);
734    };
735    if let Some(me) = o.remove(SELF_KEY)
736        && target_of(&me).is_none()
737    {
738        return Err(not_a_store_row(&me));
739    }
740    let Value::Object(o) = render_row_values(Value::Object(o)) else {
741        return Ok(shaped);
742    };
743    for (k, v) in o {
744        match k.as_str() {
745            ID_KEY => shaped.id = v.to_string(),
746            PATH_KEY => shaped.path = path_string(&v),
747            _ => {
748                shaped.proj.insert(k, v);
749            }
750        }
751    }
752    Ok(shaped)
753}
754
755/// The identity of a row the engine produced, read off the injected columns
756/// without shaping it — exactly the strings [`shape_row`] would carry — so an
757/// unshaped page can be cut by the keyset cursor before shaping (a scan that
758/// projects thousands of rows shapes fifty).
759fn raw_identity(row: &Value) -> (String, String) {
760    let get = |k: &str| {
761        row.as_object()
762            .and_then(|o| o.get(k))
763            .cloned()
764            .unwrap_or(Value::Undefined)
765    };
766    (path_string(&get(PATH_KEY)), get(ID_KEY).to_string())
767}
768
769/// A hit: `{ id, path, ...proj }` (§1.4, 3.0: the envelope keys are the
770/// attributes that read them). The envelope keys lead; a user projection of
771/// `id`/`path` collapses onto them in place (the same value, read by the
772/// same attribute).
773fn to_hit(s: Shaped) -> Value {
774    let mut hit = oqx::Object::with_capacity(s.proj.len() + 2);
775    hit.insert(HIT_ID, Value::Str(s.id));
776    hit.insert(HIT_PATH, Value::Str(s.path));
777    for (k, v) in s.proj {
778        hit.insert(k, v);
779    }
780    Value::Object(hit)
781}
782
783/// `bare` (§1.4, 4.2): the projection alone, in the query's own order. A
784/// projected `id`/`path` is an ordinary column here — the intrinsic the
785/// attribute reads, under the key the query gave it — and nothing is injected
786/// beside it.
787fn to_bare_hit(s: Shaped) -> Value {
788    Value::Object(s.proj)
789}
790
791/// The wire shape of one shaped row: the hit, or the bare hit.
792fn render_hit(s: Shaped, bare: bool) -> Json {
793    if bare {
794        to_bare_hit(s).to_canonical_json()
795    } else {
796        to_hit(s).to_canonical_json()
797    }
798}
799
800/// Top-level `select distinct`: dedup by the USER projection — every field but
801/// the envelope's `id`/`path`, which a projection of the attribute collapses
802/// onto (so `select distinct id` dedups on nothing but the empty projection,
803/// as it always has); under `bare` there is no envelope, so a projected
804/// `id`/`path` IS part of the projection and counts. Keeps the first (so the
805/// retained row carries a real identity), order-preserving. The key is the
806/// canonical JSON of the sorted `[key, value]` pairs (`JSON.stringify` in the
807/// reference).
808fn dedup_by_projection(rows: Vec<Shaped>, bare: bool) -> Vec<Shaped> {
809    let mut seen: Vec<String> = Vec::new();
810    let mut out = Vec::new();
811    for s in rows {
812        let mut pairs: Vec<(String, Value)> = s
813            .proj
814            .iter()
815            .filter(|(k, _)| bare || (*k != HIT_ID && *k != HIT_PATH))
816            .map(|(k, v)| (k.to_owned(), v.clone()))
817            .collect();
818        pairs.sort_by(|a, b| a.0.cmp(&b.0));
819        let key = Value::Array(
820            pairs
821                .into_iter()
822                .map(|(k, v)| Value::Array(vec![Value::Str(k), v]))
823                .collect(),
824        )
825        .to_canonical_json()
826        .to_string();
827        if seen.contains(&key) {
828            continue;
829        }
830        seen.push(key);
831        out.push(s);
832    }
833    out
834}
835
836/// A top-level `limit`/`offset` on the collect path is applied by the runner,
837/// so it must be a plain non-negative integer literal.
838fn const_bound(e: Option<&Expr>, word: &str) -> Result<Option<usize>> {
839    match e {
840        None => Ok(None),
841        Some(Expr::Lit {
842            value: Value::Number(n),
843            ..
844        }) if n.fract() == 0.0 && *n >= 0.0 && n.is_finite() => Ok(Some(*n as usize)),
845        Some(_) => Err(SurfaceError::filter_invalid(
846            format!("top-level {word} must be a non-negative integer literal"),
847            "OQX",
848        )),
849    }
850}
851
852/// The collect page over `rows`, whatever their representation — `ident`
853/// yields a row's `(path, id)`: the query's own `offset`/`limit` define the
854/// result SET, the keyset `cursor` (when given) cuts it, `cap` pages it.
855/// Returns the page, whether more rows followed, and the next cursor (none
856/// under a custom order, §1.4).
857fn page_rows<T>(
858    mut rows: Vec<T>,
859    ident: impl Fn(&T) -> (String, String),
860    offset: usize,
861    limit: Option<usize>,
862    cursor: Option<&str>,
863    cap: usize,
864    custom: bool,
865) -> Result<(Vec<T>, bool, Option<String>)> {
866    if offset > 0 || limit.is_some() {
867        let end = limit.map_or(rows.len(), |l| (offset + l).min(rows.len()));
868        if offset >= rows.len() {
869            rows.clear();
870        } else {
871            rows.truncate(end);
872            rows.drain(..offset);
873        }
874    }
875    if let Some(cursor) = cursor {
876        // The cursor carries the hit's (rooted) path; a 1.x cursor is refused (§1.4).
877        let parts = decode_path_cursor(cursor, "query", 2)?;
878        let (path, id) = (&parts[0], &parts[1]);
879        rows.retain(|r| {
880            let (rp, ri) = ident(r);
881            rp > *path || (rp == *path && ri > *id)
882        });
883    }
884    let truncated = rows.len() > cap;
885    rows.truncate(cap);
886    let next = if truncated && !custom {
887        rows.last().map(|last| {
888            let (p, i) = ident(last);
889            encode_cursor(&[&p, &i])
890        })
891    } else {
892        None
893    };
894    Ok((rows, truncated, next))
895}
896
897fn value_of(s: &Shaped) -> Json {
898    s.proj
899        .get(VALUE_KEY)
900        .cloned()
901        .unwrap_or(Value::Undefined)
902        .to_canonical_json()
903}
904
905/// `bare: true` needs a projection and is not the `values` form (§1.4, 4.2):
906/// without a projection a hit is only its envelope, and `values` is already
907/// the bare-values form. Checked over the resolved query where hits are built
908/// (the scalar consumers have no hits and ignore `bare`), before the engine
909/// runs, so the error never depends on the rows and the planned and in-memory
910/// paths agree trivially. The reference's `bareMisuse`.
911pub fn check_bare(q: &Query) -> Result<()> {
912    if q.values {
913        return Err(SurfaceError::filter_invalid(
914            BARE_WITH_VALUES_MESSAGE,
915            "OQX",
916        ));
917    }
918    if q.select.is_empty() {
919        return Err(SurfaceError::filter_invalid(
920            BARE_NEEDS_SELECT_MESSAGE,
921            "OQX",
922        ));
923    }
924    Ok(())
925}
926
927/// The `bare` refusals (4.2): the same text in both engines.
928pub const BARE_NEEDS_SELECT_MESSAGE: &str =
929    "`bare` needs a `select` — without a projection a hit is only its envelope";
930pub const BARE_WITH_VALUES_MESSAGE: &str =
931    "`bare` is redundant with `values` — `values` is already the bare-values form; drop one";
932
933// ---- the run --------------------------------------------------------------------------
934
935/// Run an OQX query against `repo_id` (§1.4).
936pub fn query(
937    store: &Store,
938    repo_id: &str,
939    source: &str,
940    opts: QueryOptions<'_>,
941) -> Result<OqxResult> {
942    let semantic = embed_phrases(opts.provider, collect_semantic_phrases(source))?;
943    let runner = Runner {
944        store,
945        repo_id,
946        semantic,
947        planned: !opts.in_memory,
948        now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
949    };
950    let raw = oqx::parse_string(source).map_err(|e| parse_invalid(source, &e, opts.request))?;
951    check_reserved_aliases(&raw)?;
952    // The source entry is every read: a tier-3 name on an open kind is a hint.
953    run_parsed(
954        &runner,
955        &raw,
956        opts,
957        opts.unknown_names.unwrap_or(UnknownNames::Hint),
958    )
959}
960
961// ---- the bracket-list fix (`spec/surface` §4 "Error data", 4.11) ----------------------
962//
963// OQX has no list literal; a tuple `("a", "b")` is the collection (`spec/oqx`
964// SEMANTICS §3b), so `id in ["a", "b"]` — the SQL habit — is the parse error
965// `unexpected '[' — expected a value (at offset N)`. When the source at N opens
966// a bracketed comma list of LITERALS closed by `]` (two or more members, each a
967// string, number, `true`, `false` or `null`; nothing nested), the message
968// gains `; a list is written as a tuple: (…)` and, when the request is known,
969// the error carries `fix`: the same request with that `[…]` rewritten to
970// `(…)`. Anything else at N (an identifier member, a nested bracket, one
971// member, an unclosed list) is the parser's error as it stands.
972
973const BRACKET_ERROR_HEAD: &str = "unexpected '[' — expected a value (at offset ";
974/// The suffix the bracket-list message gains (`TUPLE_HINT`).
975pub const TUPLE_HINT: &str = "; a list is written as a tuple: (…)";
976
977/// The offset `N` of `unexpected '[' — expected a value (at offset N)`, or
978/// `None` for any other message.
979fn bracket_error_offset(message: &str) -> Option<usize> {
980    let digits = message
981        .strip_prefix(BRACKET_ERROR_HEAD)?
982        .strip_suffix(')')?;
983    if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
984        return None;
985    }
986    digits.parse().ok()
987}
988
989/// The source with the bracket list at the error's offset (in code points,
990/// as the parser counts) rewritten to a tuple, or `None` when the rule does
991/// not apply (the reference's `bracketListAsTuple`).
992#[must_use]
993pub fn bracket_list_as_tuple(source: &str, message: &str) -> Option<String> {
994    let open = bracket_error_offset(message)?;
995    let mut cps: Vec<char> = source.chars().collect();
996    if cps.get(open) != Some(&'[') {
997        return None;
998    }
999    let mut i = open + 1;
1000    let mut members = 0;
1001    let close = loop {
1002        i = skip_spaces(&cps, i);
1003        let end = literal_end(&cps, i)?;
1004        members += 1;
1005        i = skip_spaces(&cps, end);
1006        match cps.get(i) {
1007            Some(',') => i += 1,
1008            Some(']') => break i,
1009            _ => return None,
1010        }
1011    };
1012    if members < 2 {
1013        return None;
1014    }
1015    cps[open] = '(';
1016    cps[close] = ')';
1017    Some(cps.into_iter().collect())
1018}
1019
1020fn skip_spaces(cps: &[char], mut i: usize) -> usize {
1021    while i < cps.len() && cps[i].is_whitespace() {
1022        i += 1;
1023    }
1024    i
1025}
1026
1027/// The index just past one OQX literal starting at `i` (a string in either
1028/// quote with `\<c>` escapes, an unsigned number, `true`/`false`/`null`), or
1029/// `None`.
1030fn literal_end(cps: &[char], i: usize) -> Option<usize> {
1031    let c = *cps.get(i)?;
1032    if c == '"' || c == '\'' {
1033        let mut j = i + 1;
1034        while j < cps.len() {
1035            if cps[j] == '\\' {
1036                j += 2;
1037            } else if cps[j] == c {
1038                return Some(j + 1);
1039            } else {
1040                j += 1;
1041            }
1042        }
1043        return None;
1044    }
1045    if c.is_ascii_digit() {
1046        let digits = |mut j: usize| {
1047            while j < cps.len() && cps[j].is_ascii_digit() {
1048                j += 1;
1049            }
1050            j
1051        };
1052        let mut j = digits(i);
1053        if cps.get(j) == Some(&'.') && cps.get(j + 1).is_some_and(char::is_ascii_digit) {
1054            j = digits(j + 1);
1055        }
1056        if matches!(cps.get(j), Some('e' | 'E')) {
1057            let mut k = j + 1;
1058            if matches!(cps.get(k), Some('+' | '-')) {
1059                k += 1;
1060            }
1061            if cps.get(k).is_some_and(char::is_ascii_digit) {
1062                j = digits(k);
1063            }
1064        }
1065        return Some(j);
1066    }
1067    for word in ["true", "false", "null"] {
1068        let n = word.len();
1069        let matches_word = cps.len() >= i + n && cps[i..i + n].iter().copied().eq(word.chars());
1070        let boundary = !cps
1071            .get(i + n)
1072            .is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '$');
1073        if matches_word && boundary {
1074            return Some(i + n);
1075        }
1076    }
1077    None
1078}
1079
1080/// The surface's `filter_invalid` for a parse error: the engine's message,
1081/// plus the tuple hint and `fix` when the bracket rule applies (the
1082/// reference's `parseInvalid`).
1083fn parse_invalid(source: &str, e: &oqx::OqxError, request: Option<&Json>) -> SurfaceError {
1084    let Some(rewritten) = bracket_list_as_tuple(source, &e.message) else {
1085        return SurfaceError::filter_invalid(e.message.clone(), "OQX");
1086    };
1087    let mut err = SurfaceError::filter_invalid(format!("{}{TUPLE_HINT}", e.message), "OQX");
1088    if let Some(Json::Object(args)) = request
1089        && let Some(d) = err.data.as_mut().and_then(Json::as_object_mut)
1090    {
1091        let mut fixed = args.clone();
1092        fixed.insert("query".to_owned(), Json::String(rewritten));
1093        d.insert(
1094            "fix".to_owned(),
1095            Json::String(omgbase_mutate::canonical_json(&Json::Object(fixed))),
1096        );
1097    }
1098    err
1099}
1100
1101/// Run an already-parsed query (§1.4) — the AST entry the selector compile
1102/// uses (`spec/mutate` §11.8: the selector's tree with the templates' slot
1103/// projections appended runs as any query does; the reserved-alias check is
1104/// the source entry's, so the appended `__omg_sel_*` items pass). `semantic`
1105/// holds the embedded phrases ([`embed_phrases`]); `opts.provider` is not
1106/// read here. Unknown names (4.6) default to [`UnknownNames::Refuse`] here:
1107/// this entry's caller is the write selector, where a name no row carries
1108/// would select every row; the source entry passes `Hint`.
1109pub fn query_ast(
1110    store: &Store,
1111    repo_id: &str,
1112    raw: &Query,
1113    opts: QueryOptions<'_>,
1114    semantic: HashMap<String, SemanticVec>,
1115) -> Result<OqxResult> {
1116    let runner = Runner {
1117        store,
1118        repo_id,
1119        semantic,
1120        planned: !opts.in_memory,
1121        now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
1122    };
1123    run_parsed(
1124        &runner,
1125        raw,
1126        opts,
1127        opts.unknown_names.unwrap_or(UnknownNames::Refuse),
1128    )
1129}
1130
1131/// One query's engine: a fresh store context per run (the planned path gives
1132/// the residual a context serving the produced rows as its root). A failure
1133/// inside a property read or row function is the engine's own error (the
1134/// context's `get` / `call_method` return `Err`); only a failed root scan,
1135/// which the `root` seam cannot raise, is read back after the run.
1136struct Runner<'a> {
1137    store: &'a Store,
1138    repo_id: &'a str,
1139    semantic: HashMap<String, SemanticVec>,
1140    planned: bool,
1141    /// The one instant of this run (§1.3): the fold and the context share it.
1142    now_ms: i64,
1143}
1144
1145impl Runner<'_> {
1146    /// Tier-3 pushdown reduces the scan in SQL and the in-memory engine
1147    /// finishes the residual over the produced rows (a declined plan, or
1148    /// `in_memory`, runs the whole query in memory over a full scan), so
1149    /// results match a pure scan. This is `oqx::PlannedEngine::run` inlined:
1150    /// the store context borrows the connection, so it cannot be the
1151    /// `'static` context a `Plan` carries.
1152    fn run(&self, q: &Query) -> Result<oqx::OqxResult> {
1153        let conn = self.store.conn();
1154        let ctx =
1155            StoreContext::new(conn, self.repo_id, self.semantic.clone()).with_now_ms(self.now_ms);
1156        let plan = if self.planned {
1157            SqlitePlanner::new(conn, self.repo_id)
1158                .try_plan(q, &[])
1159                .map_err(|e| SurfaceError::other(format!("sqlite: {e}")))?
1160        } else {
1161            None
1162        };
1163        let (ctx, residual) = match plan {
1164            // The residual's source is its one read of the rows root unless
1165            // the query text itself names `__oqx_rows__` somewhere else (a
1166            // `^`-reach, a `limit`, a nested block…): then every read must
1167            // see the rows, so they are cloned out instead of moved.
1168            Some(plan) => {
1169                let once = !mentions_outside_source(&plan.residual, oqx::ROWS_ROOT);
1170                let ctx = if once {
1171                    ctx.with_rows_root_once(plan.rows)
1172                } else {
1173                    ctx.with_rows_root(plan.rows)
1174                };
1175                (ctx, Some(plan.residual))
1176            }
1177            None => (ctx, None),
1178        };
1179        let engine = InMemoryEngine::new(ctx);
1180        let out = engine.run(residual.as_ref().unwrap_or(q), &[]);
1181        // `root` has no error channel: a store failure during a root scan was
1182        // served as an empty scan and wins over whatever the run made of it.
1183        if let Some(failed) = engine.context().take_root_failure() {
1184            return Err(failed.into());
1185        }
1186        // A root scan projected as a VALUE (`select all: ^docs`) was
1187        // materialized into its rows by the engine (`DataContext::materialize`).
1188        Ok(out?)
1189    }
1190}
1191
1192fn run_parsed(
1193    engine: &Runner<'_>,
1194    raw: &Query,
1195    opts: QueryOptions<'_>,
1196    mode: UnknownNames,
1197) -> Result<OqxResult> {
1198    check_root_spellings(raw)?;
1199    // Unknown names (spec/surface §1.5): judged over the RAW tree — before
1200    // alias resolution and the runner's own items — so an alias is never
1201    // mistaken for a row property and the verdict never depends on the rows.
1202    // The selector compiler's slot items (`__omg_sel_*`, the templates'
1203    // expressions) are not the selector: an expression reading an absent
1204    // property there is spec/mutate's `target_missing` / absent value, not an
1205    // unknown name.
1206    let own = Query {
1207        select: raw
1208            .select
1209            .iter()
1210            .filter(|it| {
1211                !RESERVED_ALIAS_PREFIXES
1212                    .iter()
1213                    .any(|p| it.name().starts_with(p))
1214            })
1215            .cloned()
1216            .collect(),
1217        ..raw.clone()
1218    };
1219    let local = VocabularyCache::default();
1220    let cache = opts.vocabulary.unwrap_or(&local);
1221    let hints = check_names(engine.store, engine.repo_id, &own, mode, cache)?;
1222    let mut result = run_resolved(engine, raw, opts)?;
1223    // `hints` is the last member, present only when there is something to say.
1224    result.hints = hints;
1225    Ok(result)
1226}
1227
1228fn run_resolved(engine: &Runner<'_>, raw: &Query, opts: QueryOptions<'_>) -> Result<OqxResult> {
1229    // The query's `select` aliases are resolved HERE, once, before the runner
1230    // renames/injects items (an engine evaluates the query it is given).
1231    let parsed = rewrite_query(&resolve_aliases(raw)?, engine.now_ms);
1232    let consumer = parsed.consumer;
1233
1234    match consumer {
1235        Consumer::Exists => {
1236            let res = engine.run(&parsed)?;
1237            let mut r = OqxResult::scalar(consumer);
1238            r.exists = Some(matches!(res, oqx::OqxResult::Exists(true)));
1239            return Ok(r);
1240        }
1241        Consumer::Count => {
1242            let res = engine.run(&parsed)?;
1243            let mut r = OqxResult::scalar(consumer);
1244            r.count = Some(match res {
1245                oqx::OqxResult::Count(n) => n,
1246                _ => 0.0,
1247            });
1248            return Ok(r);
1249        }
1250        Consumer::None => {
1251            let res = engine.run(&parsed)?;
1252            let mut r = OqxResult::scalar(consumer);
1253            r.none = Some(match res {
1254                oqx::OqxResult::None(b) => b,
1255                _ => true,
1256            });
1257            return Ok(r);
1258        }
1259        Consumer::Collect | Consumer::First | Consumer::Single => {}
1260    }
1261
1262    // A grouped query (oqx 0.24, SEMANTICS §24) yields GROUPS, not store rows:
1263    // the envelope does not apply — there is no `id`/`path` to inject and no
1264    // keyset to page by — so a hit is the group's projection as the query
1265    // wrote it (`{ layer, n }`), exactly what `bare` yields for a row query;
1266    // `bare` is accepted and changes nothing. Rows that surface as values
1267    // (`rows collect { }` with an empty projection, `collect(doc)`) render as
1268    // `{ id, path }` refs like every row-as-value. Paging is by position: the
1269    // query's own `limit`/`offset` define the set, the tool's `limit` takes
1270    // the first page, `truncated` says whether more groups follow, the cursor
1271    // is always null (as under a custom `order by`). `distinct` cannot occur
1272    // (a parse error).
1273    if parsed.group_by.is_some() {
1274        return run_grouped(engine, &parsed, consumer, opts);
1275    }
1276
1277    // A flattened query (oqx 0.25, SEMANTICS §25) yields the parent rows, one
1278    // per element: a hit is still a store row, so the envelope is injected as
1279    // usual and repeats per element (Dataview's `TABLE … FLATTEN`). What
1280    // changes is uniqueness — `(path, id)` no longer names one hit — so the
1281    // keyset cursor cannot land inside a document's elements: a flattened
1282    // query pages positionally with a null cursor, as a grouped query and a
1283    // custom `order by` do (a keyset extended with the flatten ordinal is a
1284    // later round). A bound name of `id` or `path` would shadow the envelope's
1285    // own reads (the name shadows a same-named row property on the flattened
1286    // row): refused, like a projection that collides with the envelope.
1287    let flattened = !parsed.flatten.is_empty();
1288    if let Some(e) = flatten_collision(&parsed.flatten) {
1289        return Err(e);
1290    }
1291
1292    // collect / first / single: inject id + path so every hit carries them. A
1293    // top-level `select distinct` is applied HERE, not in the engine (the
1294    // injected id/path are unique per row and would defeat the engine's
1295    // projection dedup). A top-level `values` projection runs as a RECORD
1296    // projection whose single item is renamed to VALUE_KEY.
1297    let top_distinct = parsed.distinct;
1298    let top_values = parsed.values;
1299    let user_select: Vec<SelectItem> = if top_values {
1300        parsed
1301            .select
1302            .first()
1303            .map(|it| match it {
1304                SelectItem::Field {
1305                    expr, lift, span, ..
1306                } => SelectItem::Field {
1307                    name: VALUE_KEY.to_owned(),
1308                    expr: expr.clone(),
1309                    lift: *lift,
1310                    span: *span,
1311                },
1312                SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1313                    name: VALUE_KEY.to_owned(),
1314                    op: op.clone(),
1315                    span: *span,
1316                },
1317            })
1318            .into_iter()
1319            .collect()
1320    } else {
1321        parsed.select.clone()
1322    };
1323    // `bare` (4.2): the hits are the projection alone — refused without one
1324    // and with `values`; with no envelope there is no collision to refuse
1325    // either, a projected `id`/`path` is simply a column.
1326    let bare = opts.bare;
1327    if bare {
1328        check_bare(&parsed)?;
1329    } else if !top_values && let Some(e) = envelope_collision(&user_select) {
1330        return Err(e);
1331    }
1332    let id_item = build::field(ID_KEY, build::ident("id"));
1333    let path_item = build::field(PATH_KEY, build::ident("path"));
1334    let mut select = vec![id_item, path_item];
1335    if !top_values && may_reach_non_rows(&parsed) {
1336        select.push(build::field(SELF_KEY, build::ident("$it")));
1337    }
1338    select.extend(user_select);
1339    // On the collect path the query's own limit/offset is taken out of the
1340    // engine query and applied after the runner's distinct; first/single keep
1341    // theirs (the engine's offset-aware cap is exactly right for them).
1342    let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1343    let q = Query {
1344        distinct: false,
1345        values: false,
1346        select,
1347        limit: if consumer == Consumer::Collect {
1348            None
1349        } else {
1350            parsed.limit.clone()
1351        },
1352        offset: if consumer == Consumer::Collect {
1353            None
1354        } else {
1355            parsed.offset.clone()
1356        },
1357        ..parsed.clone()
1358    };
1359    let res = engine.run(&q)?;
1360
1361    if matches!(consumer, Consumer::First | Consumer::Single) {
1362        let row = match res {
1363            oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1364            _ => None,
1365        };
1366        let mut out = OqxResult::scalar(consumer);
1367        match row {
1368            None => {
1369                if top_values {
1370                    out.values = Some(Vec::new());
1371                }
1372            }
1373            Some(r) => {
1374                let shaped = shape_row(r)?;
1375                if top_values {
1376                    out.values = Some(vec![value_of(&shaped)]);
1377                } else {
1378                    out.hits = vec![render_hit(shaped, bare)];
1379                }
1380            }
1381        }
1382        return Ok(out);
1383    }
1384
1385    // collect: keyset pagination on (path, id) when the order is the default.
1386    let rows: Vec<Value> = match res {
1387        oqx::OqxResult::Collect(rows) => rows,
1388        _ => Vec::new(),
1389    };
1390    let custom = parsed.order_by.as_ref().is_some_and(|o| !o.is_empty()) || flattened;
1391    let cursor = opts.cursor.filter(|c| !c.is_empty() && !custom);
1392    let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1393    let limit = const_bound(top_limit.as_ref(), "limit")?;
1394    let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1395    // The page runs on IDENTITY, never on the hit: a `distinct` needs every
1396    // row shaped first (its key is the projection), else the plain rows are
1397    // paged by their injected columns and only the page is shaped. The
1398    // envelope is stripped LAST (`bare`), so a bare result pages exactly like
1399    // the enveloped one.
1400    let (page, truncated, next) = if top_distinct {
1401        let shaped = rows
1402            .into_iter()
1403            .map(shape_row)
1404            .collect::<Result<Vec<_>>>()?;
1405        let shaped = dedup_by_projection(shaped, bare);
1406        page_rows(
1407            shaped,
1408            |s| (s.path.clone(), s.id.clone()),
1409            offset,
1410            limit,
1411            cursor,
1412            cap,
1413            custom,
1414        )?
1415    } else {
1416        let (page, truncated, next) =
1417            page_rows(rows, raw_identity, offset, limit, cursor, cap, custom)?;
1418        let page = page
1419            .into_iter()
1420            .map(shape_row)
1421            .collect::<Result<Vec<_>>>()?;
1422        (page, truncated, next)
1423    };
1424    let mut out = OqxResult::scalar(Consumer::Collect);
1425    out.truncated = truncated;
1426    out.cursor = next;
1427    if top_values {
1428        out.values = Some(page.iter().map(value_of).collect());
1429    } else {
1430        out.hits = page.into_iter().map(|s| render_hit(s, bare)).collect();
1431    }
1432    Ok(out)
1433}
1434
1435/// The collect/first/single path of a grouped query (see [`run_resolved`]).
1436fn run_grouped(
1437    engine: &Runner<'_>,
1438    parsed: &Query,
1439    consumer: Consumer,
1440    opts: QueryOptions<'_>,
1441) -> Result<OqxResult> {
1442    let top_values = parsed.values;
1443    // A `values` projection rides as a record under VALUE_KEY and is peeled off
1444    // the page, as on the row path.
1445    let select: Vec<SelectItem> = if top_values {
1446        parsed
1447            .select
1448            .first()
1449            .map(|it| match it {
1450                SelectItem::Field {
1451                    expr, lift, span, ..
1452                } => SelectItem::Field {
1453                    name: VALUE_KEY.to_owned(),
1454                    expr: expr.clone(),
1455                    lift: *lift,
1456                    span: *span,
1457                },
1458                SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1459                    name: VALUE_KEY.to_owned(),
1460                    op: op.clone(),
1461                    span: *span,
1462                },
1463            })
1464            .into_iter()
1465            .collect()
1466    } else {
1467        parsed.select.clone()
1468    };
1469    let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1470    let q = Query {
1471        values: false,
1472        select,
1473        limit: if consumer == Consumer::Collect {
1474            None
1475        } else {
1476            parsed.limit.clone()
1477        },
1478        offset: if consumer == Consumer::Collect {
1479            None
1480        } else {
1481            parsed.offset.clone()
1482        },
1483        ..parsed.clone()
1484    };
1485    let res = engine.run(&q)?;
1486    let project = |row: Value| -> oqx::Object {
1487        match render_row_values(row) {
1488            Value::Object(o) => o,
1489            _ => oqx::Object::new(),
1490        }
1491    };
1492    let value_of = |g: &oqx::Object| -> Json {
1493        g.get(VALUE_KEY)
1494            .cloned()
1495            .unwrap_or(Value::Undefined)
1496            .to_canonical_json()
1497    };
1498    if matches!(consumer, Consumer::First | Consumer::Single) {
1499        let row = match res {
1500            oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1501            _ => None,
1502        };
1503        let mut out = OqxResult::scalar(consumer);
1504        match row {
1505            None => {
1506                if top_values {
1507                    out.values = Some(Vec::new());
1508                }
1509            }
1510            Some(r) => {
1511                let g = project(r);
1512                if top_values {
1513                    out.values = Some(vec![value_of(&g)]);
1514                } else {
1515                    out.hits = vec![Value::Object(g).to_canonical_json()];
1516                }
1517            }
1518        }
1519        return Ok(out);
1520    }
1521    let mut groups: Vec<oqx::Object> = match res {
1522        oqx::OqxResult::Collect(rows) => rows.into_iter().map(project).collect(),
1523        _ => Vec::new(),
1524    };
1525    let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1526    let limit = const_bound(top_limit.as_ref(), "limit")?;
1527    if offset > 0 || limit.is_some() {
1528        let end = limit.map_or(groups.len(), |l| (offset + l).min(groups.len()));
1529        if offset >= groups.len() {
1530            groups.clear();
1531        } else {
1532            groups.truncate(end);
1533            groups.drain(..offset);
1534        }
1535    }
1536    let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1537    let truncated = groups.len() > cap;
1538    groups.truncate(cap);
1539    let mut out = OqxResult::scalar(Consumer::Collect);
1540    out.truncated = truncated;
1541    if top_values {
1542        out.values = Some(groups.iter().map(value_of).collect());
1543    } else {
1544        out.hits = groups
1545            .into_iter()
1546            .map(|g| Value::Object(g).to_canonical_json())
1547            .collect();
1548    }
1549    Ok(out)
1550}
1551
1552#[cfg(test)]
1553mod tests {
1554    use super::*;
1555    use oqx::ast::Where;
1556    use serde_json::json;
1557
1558    // ---- the bracket-list fix (spec/surface §4 "Error data", 4.11) ----
1559
1560    fn parse_err(src: &str) -> String {
1561        oqx::parse_string(src).unwrap_err().message
1562    }
1563
1564    #[test]
1565    fn a_bracket_list_of_literals_is_rewritten_to_a_tuple() {
1566        let cases = [
1567            (
1568                "from docs where stage in [\"nigredo\", \"albedo\"]",
1569                "from docs where stage in (\"nigredo\", \"albedo\")",
1570            ),
1571            (
1572                "from blocks where depth in [0, 1] and path == \"/index.md\"",
1573                "from blocks where depth in (0, 1) and path == \"/index.md\"",
1574            ),
1575            (
1576                "from docs where verified in [ true, 'yes', null ]",
1577                "from docs where verified in ( true, 'yes', null )",
1578            ),
1579            (
1580                "from docs where x in [1.5e3,2E-1,\"a\\\"b\"]",
1581                "from docs where x in (1.5e3,2E-1,\"a\\\"b\")",
1582            ),
1583            // the offset counts code points, as the parser does
1584            (
1585                "from docs where title in [\"é\", \"ü\"] and stage in [\"a\", \"b\"]",
1586                "from docs where title in (\"é\", \"ü\") and stage in [\"a\", \"b\"]",
1587            ),
1588        ];
1589        for (src, want) in cases {
1590            let msg = parse_err(src);
1591            assert!(msg.starts_with(BRACKET_ERROR_HEAD), "{src}: {msg}");
1592            assert_eq!(
1593                bracket_list_as_tuple(src, &msg).as_deref(),
1594                Some(want),
1595                "{src}"
1596            );
1597        }
1598    }
1599
1600    #[test]
1601    fn a_bracket_list_that_is_not_two_or_more_literals_has_no_rewrite() {
1602        for src in [
1603            "from docs where stage in [[\"nigredo\"], \"albedo\"]",
1604            "from docs where stage in [stage, \"albedo\"]",
1605            "from docs where stage in [\"nigredo\"]",
1606            "from docs where stage in [\"nigredo\", \"albedo\"",
1607            "from docs where stage in [\"nigredo\" \"albedo\"]",
1608            "from docs where stage in [truex, 1]",
1609            "from docs where stage in [-1, 2]",
1610            "from docs where stage in []",
1611        ] {
1612            let msg = parse_err(src);
1613            assert_eq!(bracket_list_as_tuple(src, &msg), None, "{src}: {msg}");
1614        }
1615        // another message, or a message whose offset is not a `[`
1616        assert_eq!(bracket_list_as_tuple("from docs", "unexpected end"), None);
1617        assert_eq!(
1618            bracket_list_as_tuple(
1619                "from docs",
1620                "unexpected '[' — expected a value (at offset 2)"
1621            ),
1622            None
1623        );
1624        assert_eq!(
1625            bracket_error_offset("unexpected '[' — expected a value (at offset x)"),
1626            None
1627        );
1628        assert_eq!(
1629            bracket_error_offset("unexpected '[' — expected a value (at offset 7)"),
1630            Some(7)
1631        );
1632    }
1633
1634    #[test]
1635    fn a_parse_error_carries_the_hint_and_the_corrected_call() {
1636        let src = "select path from docs where stage in [\"nigredo\", \"albedo\"]";
1637        let e = oqx::parse_string(src).unwrap_err();
1638        let request = json!({ "query": src, "limit": 5, "bare": true });
1639        let err = parse_invalid(src, &e, Some(&request));
1640        assert_eq!(err.code, "filter_invalid");
1641        assert_eq!(
1642            err.message,
1643            "unexpected '[' — expected a value (at offset 37); a list is written as a tuple: (…)"
1644        );
1645        let d = err.data.unwrap();
1646        assert_eq!(d["hint"], "OQX");
1647        assert_eq!(d["reason"], err.message);
1648        assert_eq!(
1649            d["fix"],
1650            r#"{"bare":true,"limit":5,"query":"select path from docs where stage in (\"nigredo\", \"albedo\")"}"#
1651        );
1652        // no request: the hint alone
1653        let err = parse_invalid(src, &e, None);
1654        assert!(err.message.ends_with(TUPLE_HINT));
1655        assert!(err.data.unwrap().get("fix").is_none());
1656        // the rule does not apply: the parser's error as it stands
1657        let src = "from docs where stage in [\"nigredo\"]";
1658        let e = oqx::parse_string(src).unwrap_err();
1659        let err = parse_invalid(src, &e, Some(&json!({ "query": src })));
1660        assert_eq!(
1661            err.message,
1662            "unexpected '[' — expected a value (at offset 25)"
1663        );
1664        assert!(err.data.unwrap().get("fix").is_none());
1665    }
1666
1667    #[test]
1668    fn row_functions_become_self_methods() {
1669        let q = oqx::parse_string(
1670            "from blocks where text(\"x\") && under_heading(\"h\") && size(attrs) > 0 && doc.path.startsWith(\"a\")",
1671        )
1672        .unwrap();
1673        let r = rewrite_query(&q, 0);
1674        let Some(Where::And { parts, .. }) = &r.r#where else {
1675            panic!("and")
1676        };
1677        let Where::Scalar { expr, .. } = &parts[0] else {
1678            panic!("scalar")
1679        };
1680        assert!(
1681            matches!(expr, Expr::Call { recv: Some(r), name, .. } if name == "text" && **r == build::ident("$self"))
1682        );
1683        let Where::Scalar { expr, .. } = &parts[2] else {
1684            panic!("scalar")
1685        };
1686        assert!(
1687            matches!(expr, Expr::Binary { left, .. } if matches!(&**left, Expr::Call { recv: None, name, .. } if name == "size"))
1688        );
1689    }
1690
1691    // ---- the constant fold (spec/surface §1.3, 4.8; the reference's dates.test.ts) ----
1692
1693    /// 2026-09-27T00:00:00.000Z — the corpus clock.
1694    const NOW: i64 = 1_790_467_200_000;
1695
1696    fn where_expr(src: &str) -> Expr {
1697        let q = oqx::parse_string(&format!("from docs where {src}")).unwrap();
1698        let Some(Where::Scalar { expr, .. }) = q.r#where else {
1699            panic!("not a scalar where")
1700        };
1701        expr
1702    }
1703
1704    /// Fold inside-out as `transform` does: children first.
1705    fn fold(src: &str) -> Expr {
1706        fn go(e: Expr) -> Expr {
1707            match e {
1708                Expr::Call {
1709                    recv,
1710                    name,
1711                    args,
1712                    span,
1713                } => fold_dates(
1714                    Expr::Call {
1715                        recv: recv.map(|r| Box::new(go(*r))),
1716                        name,
1717                        args: args.into_iter().map(go).collect(),
1718                        span,
1719                    },
1720                    NOW,
1721                ),
1722                other => other,
1723            }
1724        }
1725        go(where_expr(src))
1726    }
1727
1728    fn lit(e: &Expr) -> Option<&Value> {
1729        match e {
1730            Expr::Lit { value, .. } => Some(value),
1731            _ => None,
1732        }
1733    }
1734
1735    #[test]
1736    fn folds_date_dur_over_literals_and_date_methods_over_literal_receivers_and_arguments() {
1737        assert_eq!(
1738            lit(&fold("date(\"2026-01-05\")")),
1739            Some(&Value::from("2026-01-05"))
1740        );
1741        assert_eq!(
1742            lit(&fold("date(\"today\")")),
1743            Some(&Value::from("2026-09-27"))
1744        );
1745        assert_eq!(
1746            lit(&fold("dur(\"30 days\")")),
1747            Some(&Value::from(30.0 * 86400.0))
1748        );
1749        assert_eq!(
1750            lit(&fold("date(\"today\").minus(dur(\"30 days\"))")),
1751            Some(&Value::from("2026-08-28"))
1752        );
1753        assert_eq!(lit(&fold("date(\"x\")")), Some(&Value::Null));
1754        assert_eq!(lit(&fold("date(5)")), Some(&Value::Null));
1755        assert_eq!(lit(&fold("dur(60).dur_text()")), Some(&Value::from("1m")));
1756    }
1757
1758    #[test]
1759    fn leaves_a_non_literal_argument_an_unknown_name_and_a_wrong_arity_in_place() {
1760        for src in [
1761            "date(when)",
1762            "date(\"today\").until(when)",
1763            "size(\"x\")",
1764            "date()",
1765            "date(\"2026-01-01\").plus()",
1766            "\"x\".lower()",
1767        ] {
1768            assert!(matches!(fold(src), Expr::Call { .. }), "{src}");
1769        }
1770    }
1771
1772    #[test]
1773    fn the_runner_folds_at_its_clock_so_a_named_point_pushes_down() {
1774        let q = oqx::parse_string("from docs where updated_at < date(\"today\") && date(when) < date(\"today\").minus(dur(\"30 days\"))").unwrap();
1775        let printed = oqx::print_query(&rewrite_query(&q, NOW)).unwrap();
1776        assert!(printed.contains("updated_at < \"2026-09-27\""), "{printed}");
1777        assert!(printed.contains("date(when) < \"2026-08-28\""), "{printed}");
1778    }
1779
1780    #[test]
1781    fn path_literals_are_rooted_only_against_path_reads() {
1782        let q = oqx::parse_string(
1783            "select n: ^docs collect { path where path == ^after || \"x.md\" != dst_path } from docs where path.startsWith(\"lab/\") && doc.path == \"a.md\" && type == \"a.md\" && ^path == \"b.md\"",
1784        )
1785        .unwrap();
1786        let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1787        assert!(printed.contains("path.startsWith(\"/lab/\")"), "{printed}");
1788        assert!(printed.contains("doc.path == \"/a.md\""), "{printed}");
1789        assert!(printed.contains("type == \"a.md\""), "{printed}");
1790        assert!(printed.contains("^path == \"/b.md\""), "{printed}");
1791        assert!(printed.contains("\"/x.md\" != dst_path"), "{printed}");
1792        // a property compared with a path is the author's value: untouched
1793        assert!(printed.contains("path == ^after"), "{printed}");
1794        // `contains`/`endsWith`/`matches` and the empty string are literal
1795        let q = oqx::parse_string(
1796            "from docs where path.contains(\"a/\") && path.endsWith(\".md\") && path == \"\"",
1797        )
1798        .unwrap();
1799        let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1800        assert!(printed.contains("path.contains(\"a/\")"), "{printed}");
1801        assert!(printed.contains("path.endsWith(\".md\")"), "{printed}");
1802        assert!(printed.contains("path == \"/\""), "{printed}");
1803    }
1804
1805    #[test]
1806    fn repo_and_bare_targets_are_refused_statically() {
1807        let parse = |s: &str| oqx::parse_string(s).unwrap();
1808        for q in [
1809            "from docs where $repo",
1810            "select r: size($repo.docs) from docs",
1811            "select r: $repo.id from docs",
1812            "select r: nodes first { select x: ^$repo.id values } from docs",
1813            "select r: nodes first { select x: 0^$repo values } from docs",
1814            "$repo.docs count { }",
1815        ] {
1816            let e = check_root_spellings(&parse(q)).unwrap_err();
1817            assert_eq!(e.code, "filter_invalid", "{q}");
1818            assert_eq!(e.message, REPO_REMOVED_MESSAGE, "{q}");
1819        }
1820        // a bare `docs`/`links` at depth ≥ 1; the hint counts the carets
1821        let e = check_root_spellings(&parse("select x: docs collect { } from docs")).unwrap_err();
1822        assert_eq!(e.message, bare_target_message(Target::Docs, Some(1)));
1823        assert!(
1824            e.message
1825                .contains("did you mean `^docs` (the repository's documents)?")
1826        );
1827        let e = check_root_spellings(&parse(
1828            "select x: nodes collect { select y: size(links) } from docs",
1829        ))
1830        .unwrap_err();
1831        assert!(
1832            e.message
1833                .contains("did you mean `^^links` (the repository's links)?")
1834        );
1835        let e = check_root_spellings(&parse("docs collect { from docs }")).unwrap_err();
1836        assert!(e.message.contains("`^docs`"));
1837        // the root scope is depth 0: a bare target there is the scan
1838        for q in [
1839            "docs count { }",
1840            "from docs where nodes exists { }",
1841            "entries(docs) first { }",
1842            "select n: size(^docs), id: ^id, k: size(0^links) from docs",
1843            "from blocks where blocks exists { }", // judged by the context at read time
1844        ] {
1845            check_root_spellings(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1846        }
1847        // a `^target` reaching past the root
1848        let e = check_root_spellings(&parse("^docs count { }")).unwrap_err();
1849        assert_eq!(e.message, past_root_message(Target::Docs, 1));
1850        assert_eq!(
1851            e.message,
1852            "`^docs` reaches past the root — there is no enclosing row at this depth; at the top level the repository's documents are the bare `docs` (`docs count { … }`, `from docs`, `entries(docs)`)"
1853        );
1854        let e = check_root_spellings(&parse("select path from docs where ^^links exists { }"))
1855            .unwrap_err();
1856        assert!(
1857            e.message.contains("`^^links` reaches past the root"),
1858            "{}",
1859            e.message
1860        );
1861        // the read-time message spells out the rule
1862        let m = bare_target_message(Target::Blocks, None);
1863        assert!(m.contains("did you mean `^blocks` (the repository's blocks; one caret per enclosing block, or the absolute `0^blocks`)?"), "{m}");
1864    }
1865
1866    #[test]
1867    fn reserved_projection_names_are_refused() {
1868        let parse = |s: &str| oqx::parse_string(s).unwrap();
1869        let e = check_reserved_aliases(&parse("select __oqx_id: 1 from docs")).unwrap_err();
1870        assert_eq!(e.code, "filter_invalid");
1871        assert_eq!(
1872            e.message,
1873            "projection `__oqx_id` uses the reserved prefix `__oqx_` — choose another alias"
1874        );
1875        assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1876        let e = check_reserved_aliases(&parse("select __omg_sel_0_x: 1 from docs")).unwrap_err();
1877        assert!(
1878            e.message.contains("reserved prefix `__omg_`"),
1879            "{}",
1880            e.message
1881        );
1882        // only the top level: the runner injects beside those items alone
1883        assert!(
1884            check_reserved_aliases(&parse(
1885                "select __oqx: 1, x: nodes collect { select __oqx_y: 1 } from docs"
1886            ))
1887            .is_ok()
1888        );
1889    }
1890
1891    #[test]
1892    fn semantic_phrases_are_collected_distinct() {
1893        let phrases = collect_semantic_phrases(
1894            "select s: semantic(\"alpha\") from docs where semantic(\"alpha\") > 0.5 || nodes exists { where semantic(\"beta\") > 0 } order by semantic(\"gamma\") desc",
1895        );
1896        assert_eq!(phrases, ["alpha", "beta", "gamma"]);
1897        assert!(collect_semantic_phrases("not a query {{").is_empty());
1898        assert!(collect_semantic_phrases("from docs").is_empty());
1899    }
1900
1901    #[test]
1902    fn hits_peel_the_injected_columns() {
1903        let mut o = oqx::Object::new();
1904        o.insert(ID_KEY, Value::Str("d_1".into()));
1905        o.insert(PATH_KEY, Value::Null);
1906        o.insert("layer", Value::Str("canon".into()));
1907        let shaped = shape_row(Value::Object(o)).unwrap();
1908        assert_eq!((shaped.id.as_str(), shaped.path.as_str()), ("d_1", ""));
1909        let hit = to_hit(shaped);
1910        let ho = hit.as_object().unwrap();
1911        assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path", "layer"]);
1912        assert_eq!(ho.get("path"), Some(&Value::Str(String::new())));
1913        // A user projection of `id` collapses onto the envelope key in place
1914        // (3.0): same position, the projected value.
1915        let mut o = oqx::Object::new();
1916        o.insert(ID_KEY, Value::Str("d_1".into()));
1917        o.insert(PATH_KEY, Value::Str("a.md".into()));
1918        o.insert("id", Value::Number(7.0));
1919        let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1920        let ho = hit.as_object().unwrap();
1921        assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path"]);
1922        assert_eq!(ho.get("id"), Some(&Value::Number(7.0)));
1923        // The unshaped row's identity reads the same strings.
1924        let mut o = oqx::Object::new();
1925        o.insert(ID_KEY, Value::Str("d_1".into()));
1926        o.insert(PATH_KEY, Value::Undefined);
1927        let row = Value::Object(o);
1928        assert_eq!(raw_identity(&row), (String::new(), "d_1".to_owned()));
1929        let shaped = shape_row(row).unwrap();
1930        assert_eq!((shaped.path, shaped.id), (String::new(), "d_1".to_owned()));
1931    }
1932
1933    #[test]
1934    fn bare_hits_are_the_projection_in_its_own_order() {
1935        // `select title, id`: the projected intrinsic under the query's key, in
1936        // the query's order — nothing injected beside it.
1937        let mut o = oqx::Object::new();
1938        o.insert(ID_KEY, Value::Str("d_1".into()));
1939        o.insert(PATH_KEY, Value::Str("/a.md".into()));
1940        o.insert("title", Value::Str("T".into()));
1941        o.insert("id", Value::Str("d_1".into()));
1942        let shaped = shape_row(Value::Object(o)).unwrap();
1943        let bare = to_bare_hit(shaped);
1944        let bo = bare.as_object().unwrap();
1945        assert_eq!(bo.keys().collect::<Vec<_>>(), ["title", "id"]);
1946        // `select path: layer` is fine bare (no envelope to collide with) and
1947        // the identity the page runs on is still the row's own path.
1948        let mut o = oqx::Object::new();
1949        o.insert(ID_KEY, Value::Str("d_1".into()));
1950        o.insert(PATH_KEY, Value::Str("/a.md".into()));
1951        o.insert("path", Value::Str("canon".into()));
1952        let shaped = shape_row(Value::Object(o)).unwrap();
1953        assert_eq!(shaped.path, "/a.md");
1954        assert_eq!(
1955            render_hit(shaped, true),
1956            serde_json::json!({ "path": "canon" })
1957        );
1958        // the same row, enveloped: the user's value collapses onto the key
1959        let mut o = oqx::Object::new();
1960        o.insert(ID_KEY, Value::Str("d_1".into()));
1961        o.insert(PATH_KEY, Value::Str("/a.md".into()));
1962        o.insert("path", Value::Str("/a.md".into()));
1963        assert_eq!(
1964            render_hit(shape_row(Value::Object(o)).unwrap(), false),
1965            serde_json::json!({ "id": "d_1", "path": "/a.md" })
1966        );
1967    }
1968
1969    #[test]
1970    fn bare_needs_a_select_and_is_not_the_values_form() {
1971        let parse = |s: &str| oqx::parse_string(s).unwrap();
1972        for q in ["from docs", "from docs where layer == \"canon\""] {
1973            let e = check_bare(&parse(q)).unwrap_err();
1974            assert_eq!(e.code, "filter_invalid", "{q}");
1975            assert_eq!(e.message, BARE_NEEDS_SELECT_MESSAGE, "{q}");
1976            assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1977        }
1978        let e = check_bare(&parse("select path values from docs")).unwrap_err();
1979        assert_eq!(e.code, "filter_invalid");
1980        assert_eq!(e.message, BARE_WITH_VALUES_MESSAGE);
1981        for q in [
1982            "select path from docs",
1983            "select id, title from docs",
1984            "docs first { select t: title }",
1985        ] {
1986            check_bare(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1987        }
1988    }
1989
1990    #[test]
1991    fn a_projection_onto_the_envelope_must_be_aliased() {
1992        let items = |src: &str| oqx::parse_string(src).unwrap().select;
1993        // the attribute itself collapses (allowed)
1994        assert!(envelope_collision(&items("select id, path from docs")).is_none());
1995        assert!(
1996            envelope_collision(&items("select id: id, path: path, p: path from docs")).is_none()
1997        );
1998        // anything else landing on `id`/`path` is the error, first offender first
1999        let e = envelope_collision(&items("select data.path from docs")).unwrap();
2000        assert_eq!(e.code, "filter_invalid");
2001        assert_eq!(
2002            e.message,
2003            "projection `path` collides with the hit envelope — alias it (`p: data.path`)"
2004        );
2005        let e = envelope_collision(&items("select id: layer from docs")).unwrap();
2006        assert_eq!(
2007            e.message,
2008            "projection `id` collides with the hit envelope — alias it (`i: data.id`)"
2009        );
2010        assert!(envelope_collision(&items("select path: doc.path from blocks")).is_some());
2011        assert!(envelope_collision(&items("select path: nodes collect { } from docs")).is_some());
2012    }
2013
2014    #[test]
2015    fn a_flatten_bound_onto_the_envelope_is_refused() {
2016        let flatten = |src: &str| oqx::parse_string(src).unwrap().flatten;
2017        // a bound name beside the envelope is fine, however many flattens
2018        assert!(
2019            flatten_collision(&flatten("select path, t from docs flatten tags as t")).is_none()
2020        );
2021        assert!(
2022            flatten_collision(&flatten(
2023                "select path, b from docs flatten blocks { where type == \"task\" } as b flatten b.children as c"
2024            ))
2025            .is_none()
2026        );
2027        assert!(flatten_collision(&flatten("select path from docs")).is_none());
2028        // `as id` / `as path` would shadow the envelope's reads
2029        let e = flatten_collision(&flatten("select path from docs flatten tags as id")).unwrap();
2030        assert_eq!(e.code, "filter_invalid");
2031        assert_eq!(
2032            e.message,
2033            "flatten `as id` collides with the hit envelope — a flattened row is still the row, whose `id` the envelope reads; bind another name"
2034        );
2035        assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
2036        let e = flatten_collision(&flatten(
2037            "select title from docs flatten tags as t flatten data.paths as path",
2038        ))
2039        .unwrap();
2040        assert_eq!(
2041            e.message,
2042            "flatten `as path` collides with the hit envelope — a flattened row is still the row, whose `path` the envelope reads; bind another name"
2043        );
2044    }
2045
2046    #[test]
2047    fn a_path_read_is_the_attribute_or_a_row_handle_never_a_source_handle() {
2048        let pred = |src: &str| match oqx::parse_string(src).unwrap().r#where.unwrap() {
2049            oqx::ast::Where::Scalar { expr, .. } => expr,
2050            other => panic!("{other:?}"),
2051        };
2052        let left = |e: Expr| match e {
2053            Expr::Binary { left, .. } => *left,
2054            other => panic!("{other:?}"),
2055        };
2056        assert!(is_path_read(&left(pred("from docs where path == \"x\""))));
2057        assert!(is_path_read(&left(pred(
2058            "from blocks where doc.path == \"x\""
2059        ))));
2060        assert!(is_path_read(&left(pred("from docs where ^path == \"x\""))));
2061        assert!(is_path_read(&left(pred(
2062            "from links where dst_path == \"x\""
2063        ))));
2064        assert!(!is_path_read(&left(pred(
2065            "from docs where data.path == \"x\""
2066        ))));
2067        assert!(!is_path_read(&left(pred(
2068            "from docs where frontmatter.path == \"x\""
2069        ))));
2070    }
2071
2072    #[test]
2073    fn a_hit_that_is_not_a_store_row_fails_the_query() {
2074        // The row itself rides under SELF_KEY when the query can reach a
2075        // non-row; a string there is the `follow before` shape (§1.4, 1.5).
2076        let mut o = oqx::Object::new();
2077        o.insert(ID_KEY, Value::Undefined);
2078        o.insert(PATH_KEY, Value::Undefined);
2079        o.insert(SELF_KEY, Value::Str("/timeline/kickoff.md".into()));
2080        let e = shape_row(Value::Object(o)).unwrap_err();
2081        assert_eq!(e.code, "filter_invalid");
2082        assert_eq!(
2083            e.message,
2084            "a hit must be a document, block, node, link or revision row — the query reached a string (\"/timeline/kickoff.md\"); to follow document references held in a property use refs(<field>)"
2085        );
2086        // A tagged row under SELF_KEY passes and the key is peeled off.
2087        let mut row = oqx::Object::new();
2088        row.insert("doc_id", Value::Str("d_1".into()));
2089        row.insert("path", Value::Str("a.md".into()));
2090        let row = crate::context::tag_row(row, crate::context::Target::Docs);
2091        let mut o = oqx::Object::new();
2092        o.insert(ID_KEY, Value::Str("d_1".into()));
2093        o.insert(PATH_KEY, Value::Str("a.md".into()));
2094        o.insert(SELF_KEY, row);
2095        let hit = to_hit(shape_row(Value::Object(o)).unwrap());
2096        assert_eq!(
2097            hit.as_object().unwrap().keys().collect::<Vec<_>>(),
2098            ["id", "path"]
2099        );
2100        // Only queries that can reach a non-row project the row itself.
2101        let parse = |s: &str| oqx::parse_string(s).unwrap();
2102        assert!(!may_reach_non_rows(&parse(
2103            "from docs where layer == \"canon\""
2104        )));
2105        assert!(may_reach_non_rows(&parse("from docs follow before")));
2106        assert!(may_reach_non_rows(&parse("refs(\"/index.md\") first { }")));
2107    }
2108
2109    #[test]
2110    fn distinct_dedups_by_user_projection_first_wins() {
2111        let mk = |id: &str, t: &str| {
2112            let mut proj = oqx::Object::new();
2113            proj.insert("type", Value::Str(t.into()));
2114            Shaped {
2115                id: id.to_owned(),
2116                path: "p".to_owned(),
2117                proj,
2118            }
2119        };
2120        let out = dedup_by_projection(vec![mk("1", "a"), mk("2", "b"), mk("3", "a")], false);
2121        assert_eq!(out.len(), 2);
2122        assert_eq!(out[0].id, "1");
2123        assert_eq!(out[1].id, "2");
2124        // A projected `id` is the envelope's own selection: not part of the key
2125        // enveloped (`select distinct id` dedups on the empty projection), part
2126        // of it bare (there is no envelope — it is the projection).
2127        let with_id = |id: &str| {
2128            let mut proj = oqx::Object::new();
2129            proj.insert("id", Value::Str(id.into()));
2130            Shaped {
2131                id: id.to_owned(),
2132                path: "p".to_owned(),
2133                proj,
2134            }
2135        };
2136        assert_eq!(
2137            dedup_by_projection(vec![with_id("1"), with_id("2")], false).len(),
2138            1
2139        );
2140        assert_eq!(
2141            dedup_by_projection(vec![with_id("1"), with_id("2")], true).len(),
2142            2
2143        );
2144    }
2145
2146    #[test]
2147    fn the_page_runs_on_identity() {
2148        let rows: Vec<(String, String)> = [("/a.md", "d_1"), ("/a.md", "d_2"), ("/b.md", "d_3")]
2149            .iter()
2150            .map(|(p, i)| ((*p).to_owned(), (*i).to_owned()))
2151            .collect();
2152        let ident = |r: &(String, String)| (r.0.clone(), r.1.clone());
2153        // cap 2: truncated, the cursor is the last row's (path, id)
2154        let (page, truncated, next) =
2155            page_rows(rows.clone(), ident, 0, None, None, 2, false).unwrap();
2156        assert_eq!(page.len(), 2);
2157        assert!(truncated);
2158        let next = next.expect("a cursor");
2159        assert_eq!(next, encode_cursor(&["/a.md", "d_2"]));
2160        // the cursor resumes after it
2161        let (page, truncated, next) =
2162            page_rows(rows.clone(), ident, 0, None, Some(&next), 2, false).unwrap();
2163        assert_eq!(page, vec![("/b.md".to_owned(), "d_3".to_owned())]);
2164        assert!(!truncated && next.is_none());
2165        // the query's own offset/limit define the set; a custom order has no cursor
2166        let (page, truncated, next) =
2167            page_rows(rows.clone(), ident, 1, Some(1), None, 50, true).unwrap();
2168        assert_eq!(page, vec![("/a.md".to_owned(), "d_2".to_owned())]);
2169        assert!(!truncated && next.is_none());
2170        let (page, truncated, next) = page_rows(rows, ident, 0, None, None, 1, true).unwrap();
2171        assert_eq!(page.len(), 1);
2172        assert!(truncated && next.is_none());
2173    }
2174
2175    #[test]
2176    fn top_level_bounds_must_be_literals() {
2177        assert_eq!(const_bound(None, "limit").unwrap(), None);
2178        assert_eq!(
2179            const_bound(
2180                Some(&Expr::Lit {
2181                    value: Value::Number(3.0),
2182                    span: oqx::Span::EMPTY
2183                }),
2184                "limit"
2185            )
2186            .unwrap(),
2187            Some(3)
2188        );
2189        let e = const_bound(
2190            Some(&Expr::Lit {
2191                value: Value::Number(-1.0),
2192                span: oqx::Span::EMPTY,
2193            }),
2194            "offset",
2195        )
2196        .unwrap_err();
2197        assert_eq!(e.code, "filter_invalid");
2198        assert!(e.message.contains("top-level offset"));
2199    }
2200}