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