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