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