Skip to main content

omgbase_surface/
context.rs

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