Skip to main content

omgbase_surface/
context.rs

1//! The query binding (`spec/surface/README.md` §1): a [`DataContext`] over
2//! the store, so the `oqx` in-memory engine reproduces the whole OQX surface
3//! — roots, intrinsics, reach-through, structural relations, the edge graph,
4//! the row functions — without a bespoke compiler. Port of
5//! `packages/core/src/oqx-js/context.ts`.
6//!
7//! Rows are the store's raw column objects (blobs as hex) carrying a hidden
8//! tag column ([`TAG_KEY`]) naming their target; `get` routes field /
9//! attribute / relation resolution per target, lazily querying the store.
10//! Row functions (`text`, `under`, …) arrive as methods on the `$self`
11//! receiver (see the runner's AST rewrite), since a free function sees no row.
12//!
13//! The tier-3 planner ([`crate::planner`]) hands the rows its SQL produced
14//! back through [`StoreContext::with_rows_root`]: the context then serves them
15//! as the residual query's [`oqx::ROWS_ROOT`] scan, while every other root,
16//! relation, intrinsic and row function still reaches the store — the
17//! reference's `rowsRoot` context option.
18//!
19//! Errors travel the engine's channel: a failure inside a property read or a
20//! row function — a bare target name, a store failure — is the
21//! `Err` of `get` / `call_method` (an eval-stage [`OqxError`], since `oqx`
22//! 0.13), which aborts the run exactly like a throw from the reference's
23//! `get`; the runner maps it to `filter_invalid` with the same message. The
24//! one seam still without a channel is `root` (the engine reads a named root
25//! for the top-level source and for a caret that reaches the root scope), so
26//! a store failure during a root scan is kept in [`StoreContext::take_root_failure`]
27//! and the runner reports it after the run.
28//!
29//! One seam differs from the reference and is bridged here:
30//!
31//! * the Rust engine expands `entries(x)` in row position itself (never via
32//!   `call_function`), so the `data` / `frontmatter` / `inline` source
33//!   handles are materialized eagerly as plain objects — one key per
34//!   top-level property in key order, valued by the scalar-vs-list rule —
35//!   instead of the reference's lazy handle. `frontmatter.<k>` and
36//!   `entries(frontmatter)` read the same values either way.
37
38use std::cell::{Cell, RefCell};
39use std::collections::HashMap;
40use std::rc::Rc;
41
42use omgbase_properties::Bound;
43use omgbase_search::{cosine_bytes, sanitize_fts_query};
44use oqx::semantics::{builtin_function, builtin_method_with, make_range, string_form};
45use oqx::{
46    CompiledRegex, DataContext, Object, OqxError, RegexDialect, RowIndex, Value, compile_regex,
47};
48use rusqlite::types::{Value as SqlValue, ValueRef};
49use rusqlite::{Connection, OptionalExtension, params_from_iter};
50
51use crate::paths::{reference_path, storage_path};
52use crate::planner;
53
54/// The hidden column tagging a store row with its target.
55pub const TAG_KEY: &str = "__oqx_target";
56/// The key of a lazy ROOT SCAN marker: `{ "__oqx_scan": "docs" }` stands for
57/// `^docs` / a bare `docs` at the root until the engine reads it in row position
58/// (`to_rows`) or is about to observe it as a value (`materialize` — an
59/// operand, an argument, a projected item, a key), or probes it through
60/// `index_for`, in which case the scan never runs. The marker never reaches
61/// the language: the engine materializes every value it observes, so `==`,
62/// `in`, `entries(…)`, `size(…)`, truthiness, `distinct` and `order by` see
63/// the rows exactly as the reference's `Proxy` array shows them. A `Value` has
64/// no identity or laziness of its own, so this is the port of that handle.
65pub const SCAN_KEY: &str = "__oqx_scan";
66
67/// The four scan targets.
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
69pub enum Target {
70    Docs,
71    Blocks,
72    Nodes,
73    Links,
74}
75
76impl Target {
77    /// The root name: `docs` | `blocks` | `nodes` | `links`.
78    #[must_use]
79    pub fn as_str(self) -> &'static str {
80        match self {
81            Target::Docs => "docs",
82            Target::Blocks => "blocks",
83            Target::Nodes => "nodes",
84            Target::Links => "links",
85        }
86    }
87
88    /// The target a root name denotes, if any.
89    #[must_use]
90    pub fn parse(s: &str) -> Option<Self> {
91        Some(match s {
92            "docs" => Target::Docs,
93            "blocks" => Target::Blocks,
94            "nodes" => Target::Nodes,
95            "links" => Target::Links,
96            _ => return None,
97        })
98    }
99}
100
101/// THE NAME RULE (`spec/surface` §1.2, 3.0). `$` belongs to OQX: `$it`,
102/// `$key`, `$depth`, `$stop`, `$leaf`, `$frontier`, `$ordinal` are values the
103/// LANGUAGE injects into a scope; omgbase adds no `$` names. Every engine
104/// attribute is a bare snake_case name that WINS on collision — the convention
105/// the relations (`children`, `blocks`, `out_links`, `doc`, `frontmatter`, …)
106/// always followed: a bare name that is an engine attribute reads the
107/// engine's value, never the author's. The attributes: every row's
108/// `id`/`path`; docs' and blocks' `raw`/`hash`/`updated_at`; docs' `title`
109/// (the computed title) and `tags` (authored tags ∪ body hashtags) and
110/// `format`; a foreign key named for what it is (`doc_id`, `block_id`,
111/// `via_id`, `dst_id`, `dst_path`, `uri`), a column its column (`type`,
112/// `text`, `depth`, `ordinal`, `kind`, `predicate`, `field`). User data is
113/// bare too, read by any non-colliding key; where it collides it is reached
114/// through a source handle — `data` (the merged authored view: frontmatter +
115/// inline by the property rule, exactly the read a bare non-colliding key
116/// performs), `frontmatter`, `inline` — so `data.path` is the author's `path:`
117/// key, `data.tags` the authored list alone, `data.title` the authored title.
118/// There is no loud-error set: a bare `id`/`path`/`updated_at`/`hash`/`raw`
119/// is simply the engine's attribute (before this batch of 3.0 the attributes
120/// were `$`-prefixed and the bare spelling was an error, §9). A bare target
121/// name — `docs`, `links`, … — is a loud error of its own (`crate::query`).
122const DOC_ATTRIBUTES: [&str; 8] = [
123    "id",
124    "path",
125    "raw",
126    "hash",
127    "updated_at",
128    "title",
129    "tags",
130    "format",
131];
132const BLOCK_ATTRIBUTES: [&str; 11] = [
133    "id",
134    "path",
135    "raw",
136    "hash",
137    "updated_at",
138    "doc_id",
139    "type",
140    "text",
141    "attrs",
142    "depth",
143    "ordinal",
144];
145const NODE_ATTRIBUTES: [&str; 8] = [
146    "id", "path", "doc_id", "block_id", "kind", "name", "value", "attrs",
147];
148const EDGE_ATTRIBUTES: [&str; 13] = [
149    "id",
150    "path",
151    "doc_id",
152    "block_id",
153    "via_id",
154    "dst_id",
155    "dst_path",
156    "uri",
157    "field",
158    "dst_kind",
159    "predicate",
160    "provenance",
161    "anchor",
162];
163/// The engine attributes per kind (`spec/surface` §1.2 table).
164#[must_use]
165pub fn attributes(t: Target) -> &'static [&'static str] {
166    match t {
167        Target::Docs => &DOC_ATTRIBUTES,
168        Target::Blocks => &BLOCK_ATTRIBUTES,
169        Target::Nodes => &NODE_ATTRIBUTES,
170        Target::Links => &EDGE_ATTRIBUTES,
171    }
172}
173/// The source handles of a document (§1.2): `data` is the merged authored
174/// view (frontmatter + inline), the other two one source each.
175pub const SOURCE_HANDLES: [&str; 3] = ["data", "frontmatter", "inline"];
176
177/// The key marking a LAZY OWNER row (foreign-key elimination, `spec/surface`
178/// §1, 3.0): the owner a reach-through hands out (`doc` on a block, node or
179/// edge; `block` on a node) carries only the columns the child row already
180/// knows — `doc_id` + `path` (from the child's `doc_id`/`src_doc` and
181/// `__path`), or `block_id` + `doc_id` + `__path` — tagged with the owner's
182/// target, plus this key. `get` answers `id`, `path`, the owner's own
183/// foreign keys, its relations and its properties (which need only the id)
184/// from those columns and runs NO statement, exactly like the bare `doc_id`;
185/// the first read of anything else (`format`, `hash`, `text`, …) loads the
186/// full row ([`StoreContext::owner_full`], memoized per run). The port of the
187/// reference's `lazyOwner` Proxy. The marker is a tagged row to everything
188/// else (`identity`, `render_row_values`, `materialize`, hits) and the hidden
189/// key is stripped with the tag.
190pub const LAZY_KEY: &str = "__oqx_lazy";
191
192/// The value of a statically SINGLE relation that has no row — `dst` on an
193/// external or dangling link, `block` on a node without one (oqx 0.20,
194/// SEMANTICS §3): an object the engine treats as one row with no rows
195/// (`to_rows` empty, `is_single` true), so `dst { title }` is absent rather
196/// than `[]`, while as a VALUE it is absent (`materialize`), so `dst == null`,
197/// `has(dst)` and `dst.title` read as before. The port of the reference's
198/// `ABSENT_SINGLE`.
199pub const ABSENT_KEY: &str = "__oqx_absent";
200
201fn absent_single() -> Value {
202    let mut o = Object::with_capacity(1);
203    o.insert(ABSENT_KEY, Value::Bool(true));
204    Value::Object(o)
205}
206
207/// Whether `v` is the absent-single marker.
208fn is_absent_single(v: &Value) -> bool {
209    v.as_object()
210        .is_some_and(|o| o.len() == 1 && o.contains_key(ABSENT_KEY))
211}
212
213/// A query phrase's embedding: the model whose cache to read and the vector
214/// as a float32 little-endian blob.
215#[derive(Clone, Debug, PartialEq)]
216pub struct SemanticVec {
217    pub model: String,
218    pub vec: Vec<u8>,
219}
220
221/// The store-backed context for one repo.
222pub struct StoreContext<'a> {
223    conn: &'a Connection,
224    repo_id: String,
225    semantic: HashMap<String, SemanticVec>,
226    /// A store failure inside [`DataContext::root`], the one read the
227    /// engine's seam cannot fail through (see the module doc).
228    root_failure: RefCell<Option<OqxError>>,
229    /// The rows a tier-3 plan produced, served as [`oqx::ROWS_ROOT`].
230    rows_root: Option<RowsRoot>,
231    /// `matches()` patterns compiled during this run, by `(String(pattern),
232    /// flags)`; see [`Self::matches_memoized`].
233    regexes: RefCell<HashMap<(String, Option<String>), CompiledRegex>>,
234    /// Root scans read during this run, by target: a context lives for one
235    /// run, during which the store does not change, so `^docs` read from
236    /// every outer row is one SELECT, not one per row.
237    scans: RefCell<HashMap<Target, Rc<Vec<Value>>>>,
238    /// How many root scans actually ran (tests prove a probe runs none).
239    scans_run: Cell<usize>,
240    /// How many statements this context has run in all (`all` / `exists`),
241    /// so a test can prove a reach-through to the owner's identity runs none.
242    statements_run: Cell<usize>,
243    /// Lazy owners loaded in full during this run, by `(target, id)` — the
244    /// store does not change within a run, so an owner is read at most once.
245    owners: RefCell<HashMap<(Target, String), Option<Value>>>,
246}
247
248/// The planned rows behind [`oqx::ROWS_ROOT`]: cloned out on every read, or
249/// — when the runner has proven the residual reads the root exactly once —
250/// moved out on the first read (`once`), which spares one deep copy and one
251/// drop of every produced row.
252struct RowsRoot {
253    rows: RefCell<Option<Vec<Value>>>,
254    once: bool,
255}
256
257fn sql_value(v: ValueRef<'_>) -> Value {
258    match v {
259        ValueRef::Null => Value::Null,
260        ValueRef::Integer(i) => Value::Number(i as f64),
261        ValueRef::Real(f) => Value::Number(f),
262        ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
263        ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
264    }
265}
266
267/// A [`Value`] as a SQL parameter (`has_edge`'s destination, the planner's
268/// bound operands): booleans as 1/0 — how `json_extract` surfaces JSON
269/// booleans, so `attrs.b == true` compares against `1` — numbers as REAL
270/// (a JavaScript number binds as a double), absent as NULL.
271pub(crate) fn to_sql(v: &Value) -> SqlValue {
272    match v {
273        Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
274        Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
275        Value::Number(n) => SqlValue::Real(*n),
276        Value::Str(s) => SqlValue::Text(s.clone()),
277        Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
278    }
279}
280
281/// JavaScript `String(v)` of an argument.
282fn js_string(v: &Value) -> String {
283    v.to_string()
284}
285
286/// `String(args[0] ?? "")`.
287fn arg_or_empty(args: &[Value], i: usize) -> String {
288    match args.get(i) {
289        None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
290        Some(v) => js_string(v),
291    }
292}
293
294/// `JSON.parse` when a string, else the value (`null` → absent).
295fn parse_json(v: &Value) -> Value {
296    match v {
297        Value::Str(s) => {
298            serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
299        }
300        Value::Null | Value::Undefined => Value::Undefined,
301        other => other.clone(),
302    }
303}
304
305/// A store failure as the engine's eval error (the message the reference's
306/// raw exception would carry).
307fn sql_err(e: rusqlite::Error) -> OqxError {
308    OqxError::eval(format!("sqlite: {e}"))
309}
310
311/// The tag of a store row, if it is one.
312pub fn target_of(row: &Value) -> Option<Target> {
313    row.as_object()
314        .and_then(|o| o.get(TAG_KEY))
315        .and_then(Value::as_str)
316        .and_then(Target::parse)
317}
318
319/// Whether `row` is a lazy owner (see [`LAZY_KEY`]).
320fn is_lazy_owner(row: &Value) -> bool {
321    row.as_object().is_some_and(|o| o.contains_key(LAZY_KEY))
322}
323
324/// The target of a lazy root-scan marker, if `v` is one.
325#[must_use]
326pub fn scan_of(v: &Value) -> Option<Target> {
327    v.as_object()
328        .and_then(|o| o.get(SCAN_KEY))
329        .and_then(Value::as_str)
330        .and_then(Target::parse)
331}
332
333fn scan_marker(t: Target) -> Value {
334    let mut o = Object::with_capacity(1);
335    o.insert(SCAN_KEY, Value::Str(t.as_str().to_owned()));
336    Value::Object(o)
337}
338
339/// The four targets, in the order the root object lists them.
340const ALL_TARGETS: [Target; 4] = [Target::Docs, Target::Blocks, Target::Nodes, Target::Links];
341
342/// Whether `row` is the root object ([`StoreContext::root_object`]): a plain
343/// object whose own keys are exactly the four collections, each the lazy
344/// marker of its target. Recognized by shape — a hidden tag would show up in
345/// `entries(^$it)`, which must name exactly the collections.
346fn is_root_object(row: &Value) -> bool {
347    row.as_object().is_some_and(|o| {
348        o.len() == ALL_TARGETS.len()
349            && ALL_TARGETS
350                .iter()
351                .all(|t| o.get(t.as_str()).is_some_and(|v| scan_of(v) == Some(*t)))
352    })
353}
354
355fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
356    row.as_object()
357        .and_then(|o| o.get(key))
358        .unwrap_or(&Value::Undefined)
359}
360
361fn col_str(row: &Value, key: &str) -> String {
362    match col(row, key) {
363        Value::Undefined | Value::Null => String::new(),
364        v => js_string(v),
365    }
366}
367
368/// Strip the hidden tag from a value tree (the wire form never carries it).
369#[must_use]
370pub fn strip_tags(v: Value) -> Value {
371    match v {
372        Value::Object(o) => Value::Object(
373            o.into_iter()
374                .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
375                .map(|(k, x)| (k, strip_tags(x)))
376                .collect(),
377        ),
378        Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
379        other => other,
380    }
381}
382
383/// §1.4 rows as values (1.2): a store row that surfaces as a VALUE in a result
384/// tree — a nested `collect { }` / `first { }` / `single { }` with an empty
385/// projection, or a `values` item that is a row — renders as `{ id, path }`
386/// (3.0: the hit envelope's own keys, spelled as the attributes; the target's
387/// id column as a string; the owning document's path in the reference form,
388/// §1 "Paths": a docs row's `path`, every other row's `__path` join column,
389/// rooted), never the store row. Everything else
390/// recurses, dropping the hidden tag as [`strip_tags`].
391#[must_use]
392pub fn render_row_values(v: Value) -> Value {
393    match v {
394        Value::Object(o) => {
395            let row = Value::Object(o);
396            if let Some(t) = target_of(&row) {
397                let (id_col, path_col) = match t {
398                    Target::Docs => ("doc_id", "path"),
399                    Target::Blocks => ("block_id", "__path"),
400                    Target::Nodes => ("node_id", "__path"),
401                    Target::Links => ("edge_id", "__path"),
402                };
403                let mut out = Object::with_capacity(2);
404                out.insert(crate::query::HIT_ID, Value::Str(col_str(&row, id_col)));
405                let path = match col(&row, path_col) {
406                    Value::Undefined | Value::Null => String::new(),
407                    v => reference_path(&js_string(v)),
408                };
409                out.insert(crate::query::HIT_PATH, Value::Str(path));
410                return Value::Object(out);
411            }
412            let Value::Object(o) = row else {
413                unreachable!()
414            };
415            Value::Object(
416                o.into_iter()
417                    .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
418                    .map(|(k, x)| (k, render_row_values(x)))
419                    .collect(),
420            )
421        }
422        Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
423        other => other,
424    }
425}
426
427impl<'a> StoreContext<'a> {
428    /// A context over `conn` scoped to `repo_id`, with the query phrases'
429    /// vectors for `semantic(...)` (empty when no provider ran).
430    #[must_use]
431    pub fn new(
432        conn: &'a Connection,
433        repo_id: &str,
434        semantic: HashMap<String, SemanticVec>,
435    ) -> Self {
436        Self {
437            conn,
438            repo_id: repo_id.to_owned(),
439            semantic,
440            root_failure: RefCell::new(None),
441            rows_root: None,
442            regexes: RefCell::new(HashMap::new()),
443            scans: RefCell::new(HashMap::new()),
444            scans_run: Cell::new(0),
445            statements_run: Cell::new(0),
446            owners: RefCell::new(HashMap::new()),
447        }
448    }
449
450    /// How many statements this context has run (every `SELECT`, the root
451    /// scans included). A reach-through to the owner's identity adds none.
452    #[must_use]
453    pub fn statements_run(&self) -> usize {
454        self.statements_run.get()
455    }
456
457    /// The repository this context is scoped to.
458    #[must_use]
459    pub fn repo_id(&self) -> &str {
460        &self.repo_id
461    }
462
463    /// How many root scans (`SELECT … FROM <target>` whole) this context has
464    /// run — at most one per target per run; none for a block whose probes
465    /// the store indexes answered.
466    #[must_use]
467    pub fn scans_run(&self) -> usize {
468        self.scans_run.get()
469    }
470
471    /// The rows of a root scan, read once per run and shared.
472    pub(crate) fn scan_rows(&self, t: Target) -> oqx::Result<Rc<Vec<Value>>> {
473        if let Some(rows) = self.scans.borrow().get(&t) {
474            return Ok(Rc::clone(rows));
475        }
476        self.scans_run.set(self.scans_run.get() + 1);
477        let rows = match self.root_scan(t)? {
478            Value::Array(rows) => Rc::new(rows),
479            _ => Rc::new(Vec::new()),
480        };
481        self.scans.borrow_mut().insert(t, Rc::clone(&rows));
482        Ok(rows)
483    }
484
485    /// `refs(x)` (`spec/surface` §1.3, 1.5): the live documents the document
486    /// references held in a property name. `x` is a string, a list, or absent;
487    /// every string element that names a live document of this repo — a doc
488    /// id (`d_…`), a repo-root-absolute path (`/a/b.md`) or a bare
489    /// repo-relative path (`a/b.md`) — resolves to that document's row;
490    /// anything else (a dangling reference, a non-string element) is dropped.
491    /// Order preserved, duplicates kept. Port of the reference's `refs`.
492    fn refs(&self, x: &Value) -> oqx::Result<Value> {
493        let items: Vec<&Value> = match x {
494            Value::Undefined | Value::Null => Vec::new(),
495            Value::Array(a) => a.iter().collect(),
496            other => vec![other],
497        };
498        let mut out = Vec::new();
499        for item in items {
500            if let Value::Str(s) = item
501                && let Some(row) = self.ref_doc(s)?
502            {
503                out.push(row);
504            }
505        }
506        Ok(Value::Array(out))
507    }
508
509    /// One document reference resolved: by `path` after stripping one leading
510    /// `/`, else — when it begins with `d_` — by `doc_id`. Each is one indexed
511    /// lookup under the root scan's guards, columns and order
512    /// ([`crate::planner`]), so the row is indistinguishable from a scanned one
513    /// and the docs root is never read.
514    fn ref_doc(&self, r: &str) -> oqx::Result<Option<Value>> {
515        let t = Target::Docs;
516        let lookup = |column: &str, value: &str| -> oqx::Result<Option<Value>> {
517            let sql = format!(
518                "SELECT {} FROM {} WHERE {} AND d.{column} = ? ORDER BY {}",
519                planner::columns(t),
520                planner::from_clause(t),
521                planner::guards(t),
522                planner::order_clause(t)
523            );
524            let params = [
525                SqlValue::Text(self.repo_id.clone()),
526                SqlValue::Text(value.to_owned()),
527            ];
528            Ok(self.one(&sql, &params)?.map(|row| tag_row(row, t)))
529        };
530        if let Some(row) = lookup("path", storage_path(r))? {
531            return Ok(Some(row));
532        }
533        if r.starts_with("d_") {
534            return lookup("doc_id", r);
535        }
536        Ok(None)
537    }
538
539    /// The tagged rows of one store-index probe statement (`store_index`).
540    pub(crate) fn probe_rows(
541        &self,
542        t: Target,
543        sql: &str,
544        params: &[SqlValue],
545    ) -> oqx::Result<Vec<Value>> {
546        Ok(tag_rows(self.all(sql, params)?, t))
547    }
548
549    /// A lazy root-scan marker expanded to its rows (`Err` → recorded as the
550    /// run's root failure, served empty — the `root` discipline).
551    fn expand_scan(&self, t: Target) -> Vec<Value> {
552        match self.scan_rows(t) {
553            Ok(rows) => rows.as_ref().clone(),
554            Err(e) => {
555                let mut slot = self.root_failure.borrow_mut();
556                if slot.is_none() {
557                    *slot = Some(e);
558                }
559                Vec::new()
560            }
561        }
562    }
563
564    /// Serve `rows` — target-tagged store rows a plan produced — as the
565    /// [`oqx::ROWS_ROOT`] scan (the residual query's source). Every other
566    /// root and every relation, intrinsic and row function still hits the
567    /// store, so the residual sees exactly what a full scan would.
568    #[must_use]
569    pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
570        self.rows_root = Some(RowsRoot {
571            rows: RefCell::new(Some(rows)),
572            once: false,
573        });
574        self
575    }
576
577    /// [`Self::with_rows_root`] for a residual whose ONLY read of
578    /// [`oqx::ROWS_ROOT`] is its source scan (the runner checks the AST: the
579    /// name appears nowhere else — no `^`-reach, no `limit`/`offset`, no
580    /// nested mention): the rows are moved out on that first read instead
581    /// of deep-copied, and a second read — which cannot happen — would see
582    /// an empty scan. Same results as [`Self::with_rows_root`], one row copy
583    /// and one drop fewer.
584    #[must_use]
585    pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
586        self.rows_root = Some(RowsRoot {
587            rows: RefCell::new(Some(rows)),
588            once: true,
589        });
590        self
591    }
592
593    /// The store failure a root scan hit during the run, if any. Every other
594    /// read fails through the engine's channel (`get` / `call_method` return
595    /// `Err`); `root` has none, so it serves an empty scan and leaves the
596    /// failure here for the runner to report.
597    pub fn take_root_failure(&self) -> Option<OqxError> {
598        self.root_failure.borrow_mut().take()
599    }
600
601    // ---- SQL helpers ------------------------------------------------------------------
602
603    fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
604        self.statements_run.set(self.statements_run.get() + 1);
605        fetch_rows(self.conn, sql, params).map_err(sql_err)
606    }
607
608    fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
609        Ok(self.all(sql, params)?.into_iter().next())
610    }
611
612    fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
613        Ok(self
614            .one(sql, params)?
615            .and_then(|o| o.values().next().cloned())
616            .unwrap_or(Value::Undefined))
617    }
618
619    fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
620        self.statements_run.set(self.statements_run.get() + 1);
621        let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
622        stmt.exists(params_from_iter(params.iter()))
623            .map_err(sql_err)
624    }
625
626    fn tag_all(rows: Vec<Object>, t: Target) -> Value {
627        Value::Array(tag_rows(rows, t))
628    }
629
630    fn tag(row: Object, t: Target) -> Value {
631        tag_row(row, t)
632    }
633
634    /// The root row (`spec/surface` §1.1, 2.0; `spec/oqx` 0.18 `root_object`):
635    /// the repository, reached as `^$it` from a top-level row (`0^$it` from any
636    /// depth). Its own keys are the four collections as lazy markers, so
637    /// `entries(^$it)` names them without running a scan and `^$it.docs` is
638    /// the same scan as `^docs`; `get` answers `id` with the repository id (an
639    /// intrinsic, not an entry). Same shape as the reference's root row.
640    fn root_row(&self) -> Value {
641        let mut o = Object::with_capacity(ALL_TARGETS.len());
642        for t in ALL_TARGETS {
643            o.insert(t.as_str(), scan_marker(t));
644        }
645        Value::Object(o)
646    }
647
648    // ---- roots (ordered for a stable (path, id) default) ------------------------------
649
650    /// The joined roots drive from `docs` in `(repo_id, path)` index order
651    /// (`CROSS JOIN` fixes the loop order) and reach the rows of each doc
652    /// through their `doc_id` index, so the `ORDER BY d.path, <id>` sorts one
653    /// document's rows at a time instead of every full row of the repo in a
654    /// temp b-tree. The `+` on the inner `repo_id` term keeps it a filter
655    /// (SQLite's unary-plus idiom: the term is not used for index selection,
656    /// which — without `ANALYZE` stats — would otherwise pick the
657    /// `(repo_id, type)` index and rescan the repo per document). Same rows,
658    /// same order: the key `(path, id)` is total (`UNIQUE (repo_id, path)`;
659    /// the id is a primary key), so no plan can order them differently.
660    fn root_scan(&self, t: Target) -> oqx::Result<Value> {
661        let repo = [SqlValue::Text(self.repo_id.clone())];
662        let sql = match t {
663            Target::Docs => {
664                "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
665            }
666            Target::Blocks => {
667                "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
668                 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
669                 ORDER BY d.path, b.block_id"
670            }
671            Target::Nodes => {
672                "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
673                 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
674            }
675            Target::Links => {
676                "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
677                 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
678                 ORDER BY d.path, e.edge_id"
679            }
680        };
681        Ok(Self::tag_all(self.all(sql, &repo)?, t))
682    }
683
684    // ---- properties ---------------------------------------------------------------------
685
686    /// A property row decoded to a plain scalar; a range-shaped string stays a
687    /// string (`range(prop)` is the opt-in).
688    fn decode_prop(r: &Object) -> Value {
689        let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
690        match get("type").as_str().unwrap_or("") {
691            "number" => get("val_num"),
692            "bool" => Value::Bool(get("val_bool").truthy()),
693            "null" => Value::Null,
694            "json" => parse_json(&get("val_json")),
695            _ => get("val_text"),
696        }
697    }
698
699    /// The scalar-vs-list rule: exactly one `card = scalar` row → the scalar;
700    /// otherwise the array; no row → the nested object under `key.`.
701    fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
702        let rows = match source {
703            Some(s) => self.all(
704                "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
705                &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
706            )?,
707            None => self.all(
708                "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
709                &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
710            )?,
711        };
712        if rows.is_empty() {
713            return self.doc_prop_object(doc_id, key, source);
714        }
715        if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
716            return Ok(Self::decode_prop(&rows[0]));
717        }
718        Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
719    }
720
721    /// The nested object rebuilt from flattened dotted keys under `prefix.`
722    /// (`Undefined` when none); leaves decoded.
723    fn doc_prop_object(
724        &self,
725        doc_id: &str,
726        prefix: &str,
727        source: Option<&str>,
728    ) -> oqx::Result<Value> {
729        let like = SqlValue::Text(format!("{prefix}.%"));
730        let rows = match source {
731            Some(s) => self.all(
732                "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
733                &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
734            )?,
735            None => self.all(
736                "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
737                &[SqlValue::Text(doc_id.to_owned()), like],
738            )?,
739        };
740        if rows.is_empty() {
741            return Ok(Value::Undefined);
742        }
743        let mut out = Object::new();
744        for r in &rows {
745            let key = r.get("key").and_then(Value::as_str).unwrap_or("");
746            let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
747                .split('.')
748                .collect();
749            set_nested(&mut out, &rest, Self::decode_prop(r));
750        }
751        Ok(Value::Object(out))
752    }
753
754    /// The `data` / `frontmatter` / `inline` bag as a plain object: one entry
755    /// per top-level key in key order, each valued by [`Self::doc_prop`].
756    /// `data` (`None`) is frontmatter ∪ inline — the keys deduped, each valued
757    /// by the merged read.
758    fn doc_prop_bag(&self, doc_id: &str, source: Option<&str>) -> oqx::Result<Value> {
759        let keys = match source {
760            Some(s) => self.all(
761                "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
762                &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(s.to_owned())],
763            )?,
764            None => self.all(
765                "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source IN ('frontmatter', 'inline') AND deleted_commit IS NULL ORDER BY key",
766                &[SqlValue::Text(doc_id.to_owned())],
767            )?,
768        };
769        let mut out = Object::new();
770        for k in keys {
771            let key = k.get("key").and_then(Value::as_str).unwrap_or("");
772            let top = key.split('.').next().unwrap_or("");
773            if !out.contains_key(top) {
774                let v = self.doc_prop(doc_id, top, source)?;
775                out.insert(top, v);
776            }
777        }
778        Ok(Value::Object(out))
779    }
780
781    /// `tags` on a document: authored ∪ hashtags, each list as the property
782    /// rule reads it (a scalar-authored `tags: x` is one element), deduped by
783    /// canonical JSON of each element, authored first in authored order, then
784    /// the computed `$tags` row (body hashtags, `spec/properties` §3.3) in
785    /// document order; `Null` when both are absent.
786    fn doc_tags(&self, doc_id: &str) -> oqx::Result<Value> {
787        fn as_list(v: Value) -> Vec<Value> {
788            match v {
789                Value::Undefined | Value::Null => Vec::new(),
790                Value::Array(a) => a,
791                other => vec![other],
792            }
793        }
794        let authored = as_list(self.doc_prop(doc_id, "tags", None)?);
795        let hashtags = as_list(self.doc_prop(doc_id, "$tags", Some("computed"))?);
796        if authored.is_empty() && hashtags.is_empty() {
797            return Ok(Value::Null);
798        }
799        let mut seen: Vec<String> = Vec::new();
800        let mut out = Vec::new();
801        for v in authored.into_iter().chain(hashtags) {
802            let k = v.to_canonical_json().to_string();
803            if seen.contains(&k) {
804                continue;
805            }
806            seen.push(k);
807            out.push(v);
808        }
809        Ok(Value::Array(out))
810    }
811
812    // ---- structure ----------------------------------------------------------------------
813
814    /// The ordinal of a block's top-level ancestor (section ranges are in
815    /// top-level ordinals).
816    fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
817        let ordinal = col(block, "ordinal").clone();
818        if col(block, "parent_block").is_absent() {
819            return Ok(ordinal);
820        }
821        let ap = col_str(block, "ancestor_path");
822        let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
823            return Ok(ordinal);
824        };
825        let r = self.scalar(
826            "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
827            &[
828                SqlValue::Text(col_str(block, "doc_id")),
829                SqlValue::Text(first.to_owned()),
830            ],
831        )?;
832        Ok(if r.is_absent() { ordinal } else { r })
833    }
834
835    /// A document's live blocks in document order — pre-order over the
836    /// containment tree (children by `ordinal` under their parent; a row whose
837    /// parent is not live is a root) — tagged, with `__path` = `path`.
838    fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
839        let rows = self.all(
840            "SELECT b.*, ?1 AS __path FROM blocks b WHERE b.doc_id = ?2 AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id",
841            &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
842        )?;
843        let ids: Vec<String> = rows
844            .iter()
845            .map(|r| {
846                r.get("block_id")
847                    .and_then(Value::as_str)
848                    .unwrap_or("")
849                    .to_owned()
850            })
851            .collect();
852        let parent_index: Vec<Option<usize>> = rows
853            .iter()
854            .map(|r| {
855                r.get("parent_block")
856                    .and_then(Value::as_str)
857                    .and_then(|p| ids.iter().position(|id| id == p))
858            })
859            .collect();
860        let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
861        let mut roots = Vec::new();
862        for (i, p) in parent_index.iter().enumerate() {
863            match p {
864                Some(p) => children[*p].push(i),
865                None => roots.push(i),
866            }
867        }
868        fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
869            order.push(i);
870            for &c in &children[i] {
871                walk(c, children, order);
872            }
873        }
874        let mut order = Vec::with_capacity(rows.len());
875        for r in roots {
876            walk(r, &children, &mut order);
877        }
878        let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
879        Ok(order
880            .into_iter()
881            .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
882            .collect())
883    }
884
885    fn jattr(row: &Value, k: &str) -> Value {
886        match parse_json(col(row, "attrs")) {
887            Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
888            _ => Value::Undefined,
889        }
890    }
891
892    /// `Ok(None)` when `key` is not a relation of `t`.
893    fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
894        let path = || SqlValue::Text(col_str(row, "__path"));
895        let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
896        let doc_path = || SqlValue::Text(col_str(row, "path"));
897        let block_id = || SqlValue::Text(col_str(row, "block_id"));
898        Ok(Some(match (t, key) {
899            (Target::Docs, "nodes") => {
900                // Document order: block-less nodes first, then by the owning
901                // block's pre-order rank, `span_start`, `node_id`.
902                let rows = self.all(
903                    "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
904                    &[doc_path(), doc_id()],
905                )?;
906                let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
907                let rank: HashMap<String, usize> = blocks
908                    .iter()
909                    .enumerate()
910                    .map(|(i, b)| (col_str(b, "block_id"), i))
911                    .collect();
912                let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
913                    .into_iter()
914                    .map(|r| {
915                        let block = r.get("block_id").and_then(Value::as_str);
916                        let (has_block, rk) = match block {
917                            None => (0, 0),
918                            Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
919                        };
920                        let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
921                        let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
922                        ((has_block, rk, span, id), r)
923                    })
924                    .collect();
925                keyed.sort_by(|a, b| {
926                    a.0.0
927                        .cmp(&b.0.0)
928                        .then(a.0.1.cmp(&b.0.1))
929                        .then(a.0.2.total_cmp(&b.0.2))
930                        .then(a.0.3.cmp(&b.0.3))
931                });
932                Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
933            }
934            (Target::Docs, "blocks") => {
935                Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
936            }
937            // The linked / linking DOCUMENTS are reached through the link rows
938            // (3.0 follow-up, §9: the `out` / `in` relations are gone):
939            // `out_links collect distinct { from dst }` and `in_links collect
940            // distinct { from doc }`.
941            (Target::Docs, "out_links") => Self::tag_all(
942                self.all(
943                    "SELECT e.*, ?1 AS __path FROM edges e WHERE e.src_doc = ?2 AND e.to_commit IS NULL ORDER BY e.predicate, e.edge_id",
944                    &[doc_path(), doc_id()],
945                )?,
946                Target::Links,
947            ),
948            (Target::Docs, "in_links") => Self::tag_all(
949                self.all(
950                    "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
951                     WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
952                    &[doc_id()],
953                )?,
954                Target::Links,
955            ),
956            (Target::Blocks, "children") => Self::tag_all(
957                self.all(
958                    "SELECT b.*, ?1 AS __path FROM blocks b WHERE b.parent_block = ?2 AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id",
959                    &[path(), block_id()],
960                )?,
961                Target::Blocks,
962            ),
963            (Target::Blocks, "nodes") => Self::tag_all(
964                self.all(
965                    "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
966                    &[path(), block_id()],
967                )?,
968                Target::Nodes,
969            ),
970            (Target::Blocks, "out_links") => Self::tag_all(
971                self.all(
972                    "SELECT e.*, ?1 AS __path FROM edges e WHERE e.src_block = ?2 AND e.to_commit IS NULL ORDER BY e.predicate, e.edge_id",
973                    &[path(), block_id()],
974                )?,
975                Target::Links,
976            ),
977            (Target::Blocks, "section") => {
978                let top = to_sql(&self.top_ordinal(row)?);
979                Self::tag_all(
980                    self.all(
981                        "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
982                           AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
983                         ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
984                        &[path(), doc_id(), top.clone(), top],
985                    )?,
986                    Target::Nodes,
987                )
988            }
989            (Target::Nodes, "blocks") => {
990                let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
991                if f.is_absent() || l.is_absent() {
992                    return Ok(Some(Value::Array(Vec::new())));
993                }
994                let (f, l) = (
995                    f.as_f64().unwrap_or(f64::NAN),
996                    l.as_f64().unwrap_or(f64::NAN),
997                );
998                let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
999                let mut kept: Vec<Value> = Vec::new();
1000                for b in rows {
1001                    let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
1002                    if t >= f && t <= l {
1003                        kept.push(b);
1004                    }
1005                }
1006                Value::Array(kept)
1007            }
1008            (Target::Nodes, "subsections") => {
1009                let (f, l, lvl) = (
1010                    Self::jattr(row, "first_ordinal"),
1011                    Self::jattr(row, "last_ordinal"),
1012                    Self::jattr(row, "level"),
1013                );
1014                if f.is_absent() {
1015                    return Ok(Some(Value::Array(Vec::new())));
1016                }
1017                Self::tag_all(
1018                    self.all(
1019                        "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
1020                           AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
1021                           AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1022                        &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1023                    )?,
1024                    Target::Nodes,
1025                )
1026            }
1027            (Target::Nodes, "children") => {
1028                let (f, l, lvl) = (
1029                    Self::jattr(row, "first_ordinal"),
1030                    Self::jattr(row, "last_ordinal"),
1031                    Self::jattr(row, "level"),
1032                );
1033                if f.is_absent() {
1034                    return Ok(Some(Value::Array(Vec::new())));
1035                }
1036                Self::tag_all(
1037                    self.all(
1038                        "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
1039                           AND json_extract(i.attrs,'$.level') > ?3
1040                           AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
1041                           AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
1042                             AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
1043                             AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
1044                             AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
1045                         ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
1046                        &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1047                    )?,
1048                    Target::Nodes,
1049                )
1050            }
1051            _ => return Ok(None),
1052        }))
1053    }
1054
1055    // ---- owning-entity reach-through: foreign-key elimination ------------------------
1056    //
1057    // The owner is handed out as a LAZY row (`LAZY_KEY`) whose identity columns
1058    // are already known from the child — a block's/node's `doc_id` and `__path`
1059    // name its document, an edge's `src_doc` and `__path` its source doc, a
1060    // node's `block_id` (+ `doc_id`, `__path`) its block — so `doc.id` and
1061    // `doc.path` (and `block.id`/`block.path`/`block.doc_id` on a node)
1062    // read those columns and run NO statement, exactly like the bare `doc_id`;
1063    // the full owner row is loaded on the first read of anything else
1064    // (`doc.format`, `block.text`). The translator mirrors this in SQL
1065    // (`doc.id` → the FK column). Port of the reference's `lazyOwner` Proxy.
1066
1067    fn lazy_owner(known: &[(&str, Value)], t: Target) -> Value {
1068        let mut o = Object::with_capacity(known.len() + 2);
1069        for (k, v) in known {
1070            o.insert(*k, v.clone());
1071        }
1072        o.insert(LAZY_KEY, Value::Bool(true));
1073        tag_row(o, t)
1074    }
1075
1076    fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
1077        let id = match col(row, "doc_id") {
1078            Value::Undefined | Value::Null => col(row, "src_doc").clone(),
1079            v => v.clone(),
1080        };
1081        if id.is_absent() {
1082            return Ok(absent_single());
1083        }
1084        match col(row, "__path") {
1085            Value::Str(path) => Ok(Self::lazy_owner(
1086                &[("doc_id", id), ("path", Value::Str(path.clone()))],
1087                Target::Docs,
1088            )),
1089            _ => Ok(self
1090                .load_owner(Target::Docs, &js_string(&id))?
1091                .unwrap_or_else(absent_single)),
1092        }
1093    }
1094
1095    /// `dst` on a link row (`spec/surface` §1.2, 3.0 follow-up): the
1096    /// destination DOCUMENT row when the link points at one (`dst_kind ==
1097    /// "document"` and the document exists — a phantom target has the kind
1098    /// and no row), read by `dst_id` exactly as `dst_path` is; absent for a
1099    /// `block` or `external` destination (never reached into). `doc` stays
1100    /// the source document.
1101    fn destination_doc(&self, row: &Value) -> oqx::Result<Value> {
1102        if col(row, "dst_kind") != &Value::Str("document".to_owned()) {
1103            return Ok(absent_single());
1104        }
1105        Ok(self
1106            .load_owner(Target::Docs, &col_str(row, "dst_node"))?
1107            .unwrap_or_else(absent_single))
1108    }
1109
1110    fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
1111        let id = col(row, "block_id");
1112        if !id.truthy() {
1113            return Ok(absent_single());
1114        }
1115        match col(row, "__path") {
1116            Value::Str(path) => Ok(Self::lazy_owner(
1117                &[
1118                    ("block_id", id.clone()),
1119                    ("doc_id", col(row, "doc_id").clone()),
1120                    ("__path", Value::Str(path.clone())),
1121                ],
1122                Target::Blocks,
1123            )),
1124            _ => Ok(self
1125                .load_owner(Target::Blocks, &js_string(id))?
1126                .unwrap_or_else(absent_single)),
1127        }
1128    }
1129
1130    /// The full owner row by id (`None` when it is not in the store), read
1131    /// at most once per run.
1132    fn load_owner(&self, t: Target, id: &str) -> oqx::Result<Option<Value>> {
1133        let key = (t, id.to_owned());
1134        if let Some(v) = self.owners.borrow().get(&key) {
1135            return Ok(v.clone());
1136        }
1137        let loaded = match t {
1138            Target::Docs => self
1139                .one(
1140                    "SELECT * FROM docs WHERE doc_id = ?1",
1141                    &[SqlValue::Text(id.to_owned())],
1142                )?
1143                .map(|o| Self::tag(o, Target::Docs)),
1144            _ => self
1145                .one(
1146                    "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
1147                    &[SqlValue::Text(id.to_owned())],
1148                )?
1149                .map(|o| Self::tag(o, Target::Blocks)),
1150        };
1151        self.owners.borrow_mut().insert(key, loaded.clone());
1152        Ok(loaded)
1153    }
1154
1155    /// Whether reading `key` off a lazy owner of `t` needs the full row. The
1156    /// known columns answer the identity (`id`, `path`), the self alias, the
1157    /// relations (keyed by `doc_id` / `path` / `block_id`), the properties
1158    /// and source handles (keyed by `doc_id`), `raw`, `title` and `tags` on
1159    /// docs (by id), `updated_at` on blocks (by id), a bare target name (an
1160    /// error) and an unknown `$x` (absent); everything that reads another
1161    /// column — `format`, `hash`, `updated_at` on docs; `type`, `text`,
1162    /// `attrs`, `ordinal`, `depth`, an attrs key, `section`, `raw`/`hash` on
1163    /// blocks — loads it.
1164    fn lazy_needs_load(t: Target, key: &str) -> bool {
1165        match t {
1166            Target::Docs => matches!(key, "format" | "hash" | "updated_at"),
1167            Target::Blocks => {
1168                !matches!(
1169                    key,
1170                    "id" | "path"
1171                        | "updated_at"
1172                        | "$self"
1173                        | "doc_id"
1174                        | "block"
1175                        | "doc"
1176                        | "children"
1177                        | "nodes"
1178                        | "out_links"
1179                ) && Target::parse(key).is_none()
1180            }
1181            Target::Nodes | Target::Links => true,
1182        }
1183    }
1184
1185    /// A lazy owner with the full row in its place when `key` needs it; the
1186    /// marker itself otherwise (`None` for a row that is not a lazy owner, or
1187    /// whose owner is gone from the store — then the partial columns stand).
1188    fn resolve_lazy(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
1189        if !is_lazy_owner(row) || !Self::lazy_needs_load(t, key) {
1190            return Ok(None);
1191        }
1192        let id_col = if t == Target::Docs {
1193            "doc_id"
1194        } else {
1195            "block_id"
1196        };
1197        self.load_owner(t, &col_str(row, id_col))
1198    }
1199
1200    // ---- engine attributes --------------------------------------------------------------
1201
1202    fn null_if_absent(v: Value) -> Value {
1203        if v.is_absent() { Value::Null } else { v }
1204    }
1205
1206    /// The engine attributes (see the name rule above; `spec/surface` §1.2,
1207    /// 3.0): bare, snake_case, winning on collision. `path` (and the edge
1208    /// field `dst_path`) is the REFERENCE form (`/a/b.md`, §1 "Paths", 2.0):
1209    /// the rows carry the storage form (`path`, the `__path` join column) and
1210    /// the read roots it, so nothing below the binding changes. The runner
1211    /// roots the string literals a query compares them with. The caller has
1212    /// checked `name` against [`attributes`], so the fallthrough is unreachable
1213    /// in practice (`Undefined`).
1214    fn attribute(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
1215        let c = |k: &str| col(row, k).clone();
1216        let rooted = |k: &str| Value::Str(reference_path(&col_str(row, k)));
1217        Ok(match (t, name) {
1218            (Target::Docs, "id") => c("doc_id"),
1219            (Target::Docs, "path") => rooted("path"),
1220            (Target::Docs, "hash") => Self::null_if_absent(c("file_hash")),
1221            (Target::Docs, "updated_at") => Self::null_if_absent(self.scalar(
1222                "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
1223                &[to_sql(&c("current_rev"))],
1224            )?),
1225            // `raw`: the exact file bytes (`spec/store` §6.1, frontmatter included).
1226            (Target::Docs, "raw") => {
1227                match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
1228                    Ok(Some(s)) => Value::Str(s),
1229                    Ok(None) => Value::Null,
1230                    Err(e) => return Err(OqxError::eval(e.to_string())),
1231                }
1232            }
1233            // `title`: the computed title (`spec/properties` §3.3 — the
1234            // authored title, else the first H1, else absent → null); the
1235            // store writes it under `$title`. `data.title` is the authored
1236            // one alone.
1237            (Target::Docs, "title") => {
1238                Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
1239            }
1240            // `tags`: authored tags ∪ body `#hashtags` (the computed `$tags`
1241            // row); `data.tags` is the authored list alone.
1242            (Target::Docs, "tags") => self.doc_tags(&col_str(row, "doc_id"))?,
1243            (Target::Docs, "format") => c("format"),
1244            (Target::Blocks, "id") => c("block_id"),
1245            (Target::Blocks, "path") => rooted("__path"),
1246            // `raw`: the block's raw source bytes — the blob its `raw_hash`
1247            // names (a container's raw holds its children verbatim); `text`
1248            // is the normalized text.
1249            (Target::Blocks, "raw") => match c("raw_hash") {
1250                Value::Str(hex) => {
1251                    let hash = omgbase_store::tree::from_hex(&hex)
1252                        .map_err(|e| OqxError::eval(e.to_string()))?;
1253                    let bytes: Option<Vec<u8>> = self
1254                        .conn
1255                        .query_row(
1256                            "SELECT bytes FROM blobs WHERE hash = ?1",
1257                            rusqlite::params![hash],
1258                            |r| r.get(0),
1259                        )
1260                        .optional()
1261                        .map_err(sql_err)?;
1262                    self.statements_run.set(self.statements_run.get() + 1);
1263                    bytes.map_or(Value::Null, |b| {
1264                        Value::Str(String::from_utf8_lossy(&b).into_owned())
1265                    })
1266                }
1267                _ => Value::Null,
1268            },
1269            (Target::Blocks, "hash") => Self::null_if_absent(c("raw_hash")),
1270            (Target::Blocks, "updated_at") => Self::null_if_absent(self.scalar(
1271                "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
1272                &[to_sql(&c("block_id"))],
1273            )?),
1274            // The structural columns and the foreign keys under the surface's
1275            // names (§1.2, 3.0).
1276            (Target::Blocks, "doc_id" | "type" | "text" | "ordinal" | "depth") => c(name),
1277            (Target::Blocks | Target::Nodes, "attrs") => parse_json(&c("attrs")),
1278            (Target::Nodes, "id") => c("node_id"),
1279            (Target::Nodes, "path") => rooted("__path"),
1280            (Target::Nodes, "doc_id" | "kind" | "name" | "value") => c(name),
1281            (Target::Nodes, "block_id") => Self::null_if_absent(c("block_id")),
1282            // edges: the store's columns under the surface's names (a foreign
1283            // key is named for what it is, the `src_field` column is `field`).
1284            (Target::Links, "id") => c("edge_id"),
1285            (Target::Links, "path") => rooted("__path"),
1286            (Target::Links, "doc_id") => c("src_doc"),
1287            (Target::Links, "block_id") => Self::null_if_absent(c("src_block")),
1288            (Target::Links, "via_id") => Self::null_if_absent(c("via_node")),
1289            (Target::Links, "dst_id") => c("dst_node"),
1290            (Target::Links, "dst_path") => match self.scalar(
1291                "SELECT path FROM docs WHERE doc_id = ?1",
1292                &[to_sql(&c("dst_node"))],
1293            )? {
1294                Value::Undefined | Value::Null => Value::Null,
1295                p => Value::Str(reference_path(&js_string(&p))),
1296            },
1297            (Target::Links, "uri") => Self::null_if_absent(self.scalar(
1298                "SELECT uri FROM external_nodes WHERE node_id = ?1",
1299                &[to_sql(&c("dst_node"))],
1300            )?),
1301            (Target::Links, "field") => Self::null_if_absent(c("src_field")),
1302            (Target::Links, "predicate" | "provenance" | "dst_kind" | "anchor") => c(name),
1303            _ => Value::Undefined,
1304        })
1305    }
1306
1307    // ---- row functions (methods on `$self`) ----------------------------------------------
1308
1309    fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
1310        Some(Err(OqxError::eval(msg)))
1311    }
1312
1313    fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
1314        (t != want).then(|| {
1315            Err(OqxError::eval(format!(
1316                "{name}() is only available on the {} target",
1317                want.as_str()
1318            )))
1319        })
1320    }
1321
1322    fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
1323        r.map(Value::Bool)
1324    }
1325
1326    fn row_method(
1327        &self,
1328        name: &str,
1329        row: &Value,
1330        t: Target,
1331        args: &[Value],
1332    ) -> Option<oqx::Result<Value>> {
1333        let c = |k: &str| col(row, k).clone();
1334        match name {
1335            "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
1336            "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
1337            "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1338                Some(Self::sql_result(self.exists(
1339                    "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
1340                    &[to_sql(&c("block_id"))],
1341                )))
1342            }),
1343            "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1344                Some(self.scalar(
1345                    "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
1346                    &[to_sql(&c("block_id"))],
1347                ))
1348            }),
1349            "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1350                Some(
1351                    self.scalar(
1352                        "SELECT type FROM blocks WHERE block_id = ?1",
1353                        &[to_sql(&c("parent_block"))],
1354                    )
1355                    .map(Self::null_if_absent),
1356                )
1357            }),
1358            "has_edge" => {
1359                let pred = js_string(args.first().unwrap_or(&Value::Undefined));
1360                let (src_col, src_val) = if t == Target::Blocks {
1361                    ("src_block", c("block_id"))
1362                } else {
1363                    ("src_doc", c("doc_id"))
1364                };
1365                let r = if args.len() >= 2 {
1366                    self.exists(
1367                        &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
1368                        &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
1369                    )
1370                } else {
1371                    self.exists(
1372                        &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
1373                        &[to_sql(&src_val), SqlValue::Text(pred)],
1374                    )
1375                };
1376                Some(Self::sql_result(r))
1377            }
1378            "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1379                let target = js_string(args.first().unwrap_or(&Value::Undefined));
1380                let ap = col_str(row, "ancestor_path");
1381                Some(Ok(Value::Bool(
1382                    ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
1383                )))
1384            }),
1385            "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1386                let text = js_string(args.first().unwrap_or(&Value::Undefined));
1387                let top = match self.top_ordinal(row) {
1388                    Ok(v) => to_sql(&v),
1389                    Err(e) => return Some(Err(e)),
1390                };
1391                Some(Self::sql_result(self.exists(
1392                    "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
1393                     WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
1394                    &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
1395                )))
1396            }),
1397            // `within(t)`: a doc id, else a path in either form (`/texts/*`,
1398            // `texts/*`, `texts/x.md`) matched against the storage path.
1399            "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1400                let target = js_string(args.first().unwrap_or(&Value::Undefined));
1401                if target.starts_with("d_") {
1402                    return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
1403                }
1404                let target = storage_path(&target).to_owned();
1405                if target.contains('*') {
1406                    let like = glob_to_like(&target, false);
1407                    return Some(Self::sql_result(self.exists(
1408                        "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
1409                        &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
1410                    )));
1411                }
1412                Some(Ok(Value::Bool(col_str(row, "__path") == target)))
1413            }),
1414            "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1415                let kind = js_string(args.first().unwrap_or(&Value::Undefined));
1416                let ap: Vec<String> = col_str(row, "ancestor_path")
1417                    .split('/')
1418                    .filter(|s| !s.is_empty())
1419                    .map(str::to_owned)
1420                    .collect();
1421                if ap.is_empty() {
1422                    return Some(Ok(Value::Bool(false)));
1423                }
1424                let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
1425                let placeholders = placeholders.join(",");
1426                let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
1427                let n = params.len();
1428                params.push(SqlValue::Text(kind));
1429                let r = match args.get(1) {
1430                    Some(v) if !v.is_absent() => {
1431                        let nm = js_string(v);
1432                        params.push(SqlValue::Text(nm.clone()));
1433                        params.push(SqlValue::Text(nm));
1434                        self.exists(
1435                            &format!(
1436                                "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
1437                                n + 1,
1438                                n + 2,
1439                                n + 3
1440                            ),
1441                            &params,
1442                        )
1443                    }
1444                    _ => self.exists(
1445                        &format!(
1446                            "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
1447                            n + 1
1448                        ),
1449                        &params,
1450                    ),
1451                };
1452                Some(Self::sql_result(r))
1453            }),
1454            "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1455                Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
1456            }),
1457            "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1458                Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
1459            }),
1460            _ => None,
1461        }
1462    }
1463
1464    fn key_path(row: &Value, path: &str, kind: &str) -> Value {
1465        let key = if kind == "json" {
1466            let mut p = path;
1467            p = p.strip_prefix('#').unwrap_or(p);
1468            p = p.strip_prefix('/').unwrap_or(p);
1469            p.split('/').collect::<Vec<_>>().join(".")
1470        } else {
1471            path.to_owned()
1472        };
1473        let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
1474        if !col_str(row, "type").starts_with(&format!("{kind}:")) {
1475            return Value::Bool(false);
1476        }
1477        let k = Self::jattr(row, "key");
1478        Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
1479    }
1480
1481    fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
1482        if t == Target::Links {
1483            return Err(OqxError::eval(
1484                "text(...) is not available on the links target",
1485            ));
1486        }
1487        let m = sanitize_fts_query(terms);
1488        if m.is_empty() {
1489            return Ok(Value::Bool(false));
1490        }
1491        let r = match t {
1492            Target::Docs => self.exists(
1493                "SELECT 1 FROM blocks_fts JOIN blocks b ON b.rowid = blocks_fts.rowid WHERE b.doc_id = ?1 AND blocks_fts MATCH ?2 LIMIT 1",
1494                &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1495            ),
1496            Target::Nodes => self.exists(
1497                "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1498                &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1499            ),
1500            _ => self.exists(
1501                "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1502                &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1503            ),
1504        };
1505        Self::sql_result(r)
1506    }
1507
1508    /// `recv.matches(pattern[, flags])` exactly as the builtin computes it —
1509    /// an absent receiver is `false` before the pattern is looked at, the
1510    /// pattern is `String(args[0])`, the flags are `args[1]`, then
1511    /// [`CompiledRegex::is_match`] on the receiver's string form — with the
1512    /// compiled pattern held for the run. The builtin's own cache hands out
1513    /// clones of the `regex::Regex`, and a clone starts with an empty
1514    /// search-cache pool, so a scan paid a lazy-DFA cache allocation (and its
1515    /// drop) per row. Flags that are not a string, and patterns that do not
1516    /// compile, are left to the builtin so its errors are reported verbatim.
1517    fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1518        let flags = match args.get(1) {
1519            None | Some(Value::Undefined) | Some(Value::Null) => None,
1520            Some(Value::Str(s)) => Some(s.clone()),
1521            Some(_) => return None,
1522        };
1523        let Some(subject) = string_form(recv) else {
1524            return Some(Ok(Value::Bool(false)));
1525        };
1526        let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1527        let key = (pattern, flags);
1528        let mut memo = self.regexes.borrow_mut();
1529        if !memo.contains_key(&key) {
1530            let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1531            match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1532                Ok(re) => {
1533                    memo.insert(key.clone(), re);
1534                }
1535                Err(_) => return None,
1536            }
1537        }
1538        Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1539    }
1540
1541    fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1542        if matches!(t, Target::Nodes | Target::Links) {
1543            return Err(OqxError::eval(
1544                "semantic(...) is available on the docs and blocks targets",
1545            ));
1546        }
1547        let Some(resolved) = self.semantic.get(phrase) else {
1548            return Err(OqxError::eval(format!(
1549                "semantic({}) needs an embedding provider; none is configured for this query",
1550                serde_json::Value::String(phrase.to_owned())
1551            )));
1552        };
1553        let vec: oqx::Result<Option<Vec<u8>>> = match t {
1554            Target::Docs => self
1555                .conn
1556                .query_row(
1557                    "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1558                    rusqlite::params![col_str(row, "doc_id"), resolved.model],
1559                    |r| r.get(0),
1560                )
1561                .optional()
1562                .map_err(sql_err),
1563            // The row for the block's current `(raw_hash, ctx_hash)` only
1564            // (`spec/search` §3, 1.1): a stale context row is never read.
1565            _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1566                .map_err(|e| OqxError::eval(e.to_string())),
1567        };
1568        match vec? {
1569            Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1570            None => Ok(Value::Null),
1571        }
1572    }
1573}
1574
1575/// Run `sql` and read every row as a column object (blobs as hex, integers
1576/// and reals as numbers) — the one shape a store row ever has in a query.
1577pub(crate) fn fetch_rows(
1578    conn: &Connection,
1579    sql: &str,
1580    params: &[SqlValue],
1581) -> rusqlite::Result<Vec<Object>> {
1582    let mut stmt = conn.prepare_cached(sql)?;
1583    let names: Vec<String> = stmt
1584        .column_names()
1585        .iter()
1586        .map(|s| (*s).to_owned())
1587        .collect();
1588    let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1589        // One slot beyond the columns: every row read here is tagged next
1590        // ([`tag_row`]), and the tag must not regrow the entries per row.
1591        let mut o = Object::with_capacity(names.len() + 1);
1592        for (i, name) in names.iter().enumerate() {
1593            o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1594        }
1595        Ok(o)
1596    })?;
1597    rows.collect()
1598}
1599
1600/// Tag a store row with its target so the context resolves it (the
1601/// reference's `tagRows`; the planner hands produced rows back this way).
1602pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1603    row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1604    Value::Object(row)
1605}
1606
1607/// [`tag_row`] over a result set.
1608pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1609    rows.into_iter().map(|r| tag_row(r, t)).collect()
1610}
1611
1612/// `cur[seg] = {}` down the path, then the leaf (a scalar in the way is
1613/// replaced by an object; an existing key keeps its position).
1614fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1615    let Some((first, rest)) = path.split_first() else {
1616        return;
1617    };
1618    if rest.is_empty() {
1619        out.insert(*first, leaf);
1620        return;
1621    }
1622    let mut child = match out.get(first) {
1623        Some(Value::Object(o)) => o.clone(),
1624        _ => Object::new(),
1625    };
1626    set_nested(&mut child, rest, leaf);
1627    out.insert(*first, Value::Object(child));
1628}
1629
1630/// A `*` glob as a `LIKE` pattern with `ESCAPE '\'`; `escape_backslash`
1631/// also escapes `\` (the list surfaces do, `within` does not).
1632#[must_use]
1633pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1634    let mut out = String::with_capacity(glob.len() + 4);
1635    for ch in glob.chars() {
1636        match ch {
1637            '%' | '_' => {
1638                out.push('\\');
1639                out.push(ch);
1640            }
1641            '\\' if escape_backslash => out.push_str("\\\\"),
1642            '*' => out.push('%'),
1643            c => out.push(c),
1644        }
1645    }
1646    out
1647}
1648
1649impl DataContext for StoreContext<'_> {
1650    fn root(&self, name: &str) -> Value {
1651        if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1652            let mut slot = rr.rows.borrow_mut();
1653            let rows = if rr.once { slot.take() } else { slot.clone() };
1654            return Value::Array(rows.unwrap_or_default());
1655        }
1656        // The root scope's names (`spec/oqx` §2 step 5 at the root): the four
1657        // lazy scans and `id`, the repository id — so `^docs` / `0^docs` from
1658        // any row is the scan and `^id` / `0^id` the id.
1659        if name == "id" {
1660            return Value::Str(self.repo_id.clone());
1661        }
1662        // A root scan is handed out LAZILY (see `SCAN_KEY`): `to_rows` runs it
1663        // — once per run — and `index_for` probes it without running it.
1664        Target::parse(name).map_or(Value::Undefined, scan_marker)
1665    }
1666
1667    /// The root scope's row (oqx 0.18): the repository, so `^$it` from a
1668    /// top-level row is the root row and `^$it.docs` the docs scan.
1669    fn root_object(&self) -> Value {
1670        self.root_row()
1671    }
1672
1673    fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1674        if row.is_absent() {
1675            return Ok(Value::Undefined);
1676        }
1677        // The root row: `^$it.id` is the repository id, `^$it.<target>` the
1678        // lazy root scan (the same marker `^<target>` / `root(name)` hands out).
1679        if is_root_object(row) {
1680            if key == "id" {
1681                return Ok(Value::Str(self.repo_id.clone()));
1682            }
1683            return Ok(Target::parse(key).map_or(Value::Undefined, scan_marker));
1684        }
1685        let Some(t) = target_of(row) else {
1686            // A plain value (parsed attrs, a property bag, a lifted element).
1687            return Ok(oqx::DefaultContext::read(row, key));
1688        };
1689        // A lazy owner (`LAZY_KEY`) answers its identity, relations and
1690        // properties from the columns it carries; any other key loads the row.
1691        if let Some(full) = self.resolve_lazy(row, t, key)? {
1692            return self.get(&full, key);
1693        }
1694        // `$self` = the current row: the runner rewrites row functions
1695        // (text/under/…) to `$self.fn(…)` so they arrive via `call_method` with
1696        // the row as receiver. Any other `$` name is OQX's (resolved by the
1697        // engine before the row) or unknown — never an omgbase attribute.
1698        if key == "$self" {
1699            return Ok(row.clone());
1700        }
1701        // engine attributes win on collision (the name rule above).
1702        if attributes(t).contains(&key) {
1703            return self.attribute(row, t, key);
1704        }
1705        // self-alias namespaces
1706        match (t, key) {
1707            (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1708                return Ok(row.clone());
1709            }
1710            (_, "doc") => return self.owning_doc(row),
1711            (Target::Nodes, "block") => return self.owning_block(row),
1712            (Target::Links, "dst") => return self.destination_doc(row),
1713            _ => {}
1714        }
1715        if let Some(v) = self.relation(row, t, key)? {
1716            return Ok(v);
1717        }
1718        // A bare target name that is not one of this row's relations reads a
1719        // property the row has none of (`spec/surface` §1.1, 2.0): loud, naming
1720        // the root-row spelling. `docs`/`links` never get here (the runner
1721        // refuses them statically); `blocks` on a block or edge row and `nodes`
1722        // on a node or edge row do. The reference throws from `get` too.
1723        if let Some(named) = Target::parse(key) {
1724            return Err(OqxError::eval(crate::query::bare_target_message(
1725                named, None,
1726            )));
1727        }
1728        // the author's data.
1729        Ok(match t {
1730            Target::Docs => {
1731                let doc_id = col_str(row, "doc_id");
1732                // The source handles: `data` is the merged authored view
1733                // (frontmatter + inline), the other two one source each;
1734                // `data.<k>` reaches an authored key whose bare name is an
1735                // engine attribute (`data.path`, `data.tags`).
1736                if SOURCE_HANDLES.contains(&key) {
1737                    let source = (key != "data").then_some(key);
1738                    return self.doc_prop_bag(&doc_id, source);
1739                }
1740                return self.doc_prop(&doc_id, key, None);
1741            }
1742            // blocks / nodes: a bare non-structural identifier flattens into
1743            // `attrs.<k>` (the attributes above win on collision); an absent
1744            // key is undefined, matching a doc's missing frontmatter key.
1745            Target::Blocks | Target::Nodes => Self::jattr(row, key),
1746            Target::Links => Value::Undefined,
1747        })
1748    }
1749
1750    fn to_rows(&self, value: &Value) -> Vec<Value> {
1751        match value {
1752            Value::Undefined | Value::Null => Vec::new(),
1753            Value::Array(a) => a.clone(),
1754            other if is_absent_single(other) => Vec::new(),
1755            other => match scan_of(other) {
1756                Some(t) => self.expand_scan(t),
1757                None => vec![other.clone()],
1758            },
1759        }
1760    }
1761
1762    /// Row position (spec/oqx §3, 0.20): a lazy root scan is a collection; a
1763    /// store row, a lazy owner, the absent-single marker and any other object
1764    /// are one row.
1765    fn is_single(&self, value: &Value) -> bool {
1766        scan_of(value).is_none()
1767    }
1768
1769    /// A lazy root-scan marker observed as a VALUE is its rows (read once per
1770    /// run); the absent-single marker is absent; everything else is itself. A
1771    /// store row is an object with several columns and the markers have exactly
1772    /// one key, so the common case is one length check.
1773    fn materialize(&self, value: Value) -> Value {
1774        if value.as_object().is_some_and(|o| o.len() == 1) {
1775            if let Some(t) = scan_of(&value) {
1776                return Value::Array(self.expand_scan(t));
1777            }
1778            if is_absent_single(&value) {
1779                return Value::Undefined;
1780            }
1781        }
1782        value
1783    }
1784
1785    fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
1786        crate::store_index::index_for(self, scan_of(collection)?, path)
1787    }
1788
1789    fn identity(&self, row: &Value) -> Value {
1790        match target_of(row) {
1791            Some(Target::Docs) => col(row, "doc_id").clone(),
1792            Some(Target::Blocks) => col(row, "block_id").clone(),
1793            Some(Target::Nodes) => col(row, "node_id").clone(),
1794            Some(Target::Links) => col(row, "edge_id").clone(),
1795            None => row.clone(),
1796        }
1797    }
1798
1799    fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
1800        // `size(^docs)`, `list(…)`, `has(…)`: the engine has materialized a
1801        // lazy scan argument into its rows before the call.
1802        if name == "refs" {
1803            return Some(self.refs(args.first().unwrap_or(&Value::Undefined)));
1804        }
1805        if name == "range" {
1806            let x = args.first().unwrap_or(&Value::Undefined);
1807            return Some(Ok(match x {
1808                Value::Range(_) => x.clone(),
1809                Value::Str(s) => match omgbase_properties::detect_range(s) {
1810                    Some(r) => {
1811                        let b = |b: &Bound| match b {
1812                            Bound::Open => Value::Undefined,
1813                            Bound::Num(n) => Value::Number(*n),
1814                            Bound::Iso(s) => Value::Str(s.clone()),
1815                        };
1816                        Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
1817                    }
1818                    None => Value::Null,
1819                },
1820                _ => Value::Null,
1821            }));
1822        }
1823        builtin_function(name, args)
1824    }
1825
1826    fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1827        // `^docs.size()`: the receiver, a value, is materialized by the
1828        // engine before the call.
1829        if let Some(t) = target_of(recv) {
1830            // A row function reads the row's columns: a lazy owner is loaded first.
1831            let full;
1832            let recv = if is_lazy_owner(recv) {
1833                let id_col = if t == Target::Docs {
1834                    "doc_id"
1835                } else {
1836                    "block_id"
1837                };
1838                match self.load_owner(t, &col_str(recv, id_col)) {
1839                    Ok(Some(v)) => {
1840                        full = v;
1841                        &full
1842                    }
1843                    Ok(None) => recv,
1844                    Err(e) => return Some(Err(e)),
1845                }
1846            } else {
1847                recv
1848            };
1849            if let Some(r) = self.row_method(name, recv, t, args) {
1850                return Some(r);
1851            }
1852        } else if matches!(
1853            name,
1854            "text"
1855                | "semantic"
1856                | "under"
1857                | "under_heading"
1858                | "within"
1859                | "under_kind"
1860                | "yaml_path"
1861                | "json_pointer"
1862                | "has_edge"
1863                | "has_anchor"
1864                | "child_count"
1865                | "parent_type"
1866        ) {
1867            return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/links row"));
1868        }
1869        if name == "matches" {
1870            if let Some(r) = self.matches_memoized(recv, args) {
1871                return Some(r);
1872            }
1873        }
1874        builtin_method_with(RegexDialect::Oqx, name, recv, args)
1875    }
1876}
1877
1878#[cfg(test)]
1879mod tests {
1880    use super::*;
1881
1882    #[test]
1883    fn glob_to_like_escapes() {
1884        assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
1885        assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
1886        assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
1887    }
1888
1889    #[test]
1890    fn rows_surfacing_as_values_render_id_and_path() {
1891        // §1.4 (1.2, keys renamed in 3.0): a tagged store row anywhere in a
1892        // value tree is `{ id, path }`; untagged records keep their keys,
1893        // minus the tag.
1894        let mut node = Object::new();
1895        node.insert("node_id", Value::Str("n_1".into()));
1896        node.insert("attrs", Value::Str("{\"checked\":true}".into()));
1897        node.insert("__path", Value::Str("a.md".into()));
1898        let mut doc = Object::new();
1899        doc.insert("doc_id", Value::Str("d_0".into()));
1900        doc.insert("path", Value::Str("a.md".into()));
1901        doc.insert("blob", Value::Str("ff".into()));
1902        let mut record = Object::new();
1903        record.insert(TAG_KEY, Value::Str("junk".into()));
1904        record.insert(
1905            "tasks",
1906            Value::Array(vec![
1907                tag_row(node, Target::Nodes),
1908                tag_row(doc, Target::Docs),
1909            ]),
1910        );
1911        let out = render_row_values(Value::Object(record));
1912        let o = out.as_object().unwrap();
1913        assert!(o.get(TAG_KEY).is_none());
1914        let tasks = o.get("tasks").unwrap().as_array().unwrap();
1915        let keys = |v: &Value| -> Vec<String> {
1916            v.as_object()
1917                .unwrap()
1918                .iter()
1919                .map(|(k, _)| k.to_owned())
1920                .collect()
1921        };
1922        assert_eq!(keys(&tasks[0]), ["id", "path"]);
1923        assert_eq!(
1924            tasks[0].as_object().unwrap().get("id"),
1925            Some(&Value::Str("n_1".into()))
1926        );
1927        assert_eq!(
1928            tasks[0].as_object().unwrap().get("path"),
1929            Some(&Value::Str("/a.md".into()))
1930        );
1931        assert_eq!(keys(&tasks[1]), ["id", "path"]);
1932        assert_eq!(
1933            tasks[1].as_object().unwrap().get("id"),
1934            Some(&Value::Str("d_0".into()))
1935        );
1936        // An id column that is not a string still renders as its string form.
1937        let mut edge = Object::new();
1938        edge.insert("edge_id", Value::Number(7.0));
1939        let e = render_row_values(tag_row(edge, Target::Links));
1940        assert_eq!(
1941            e.as_object().unwrap().get("id"),
1942            Some(&Value::Str("7".into()))
1943        );
1944        assert_eq!(
1945            e.as_object().unwrap().get("path"),
1946            Some(&Value::Str(String::new()))
1947        );
1948    }
1949
1950    #[test]
1951    fn nested_property_objects_rebuild() {
1952        let mut o = Object::new();
1953        set_nested(&mut o, &["a", "b"], Value::Number(1.0));
1954        set_nested(&mut o, &["a", "c"], Value::Number(2.0));
1955        set_nested(&mut o, &["d"], Value::Str("x".into()));
1956        let a = o.get("a").unwrap().as_object().unwrap();
1957        assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
1958        assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
1959        assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
1960        // A scalar in the way is replaced by an object.
1961        set_nested(&mut o, &["d", "e"], Value::Bool(true));
1962        assert!(o.get("d").unwrap().as_object().is_some());
1963    }
1964
1965    #[test]
1966    fn json_and_sql_bridges() {
1967        assert_eq!(
1968            parse_json(&Value::Str("{\"a\":1}".into()))
1969                .as_object()
1970                .unwrap()
1971                .get("a"),
1972            Some(&Value::Number(1.0))
1973        );
1974        assert_eq!(
1975            parse_json(&Value::Str("nope".into())),
1976            Value::Str("nope".into())
1977        );
1978        assert_eq!(parse_json(&Value::Null), Value::Undefined);
1979        assert_eq!(arg_or_empty(&[], 0), "");
1980        assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
1981    }
1982}