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::RefCell;
39use std::collections::HashMap;
40
41use omgbase_properties::Bound;
42use omgbase_search::{cosine_bytes, sanitize_fts_query};
43use oqx::semantics::{builtin_function, builtin_method_with, make_range};
44use oqx::{DataContext, Object, OqxError, RegexDialect, Value};
45use rusqlite::types::{Value as SqlValue, ValueRef};
46use rusqlite::{Connection, OptionalExtension, params_from_iter};
47
48/// The hidden column tagging a store row with its target.
49pub const TAG_KEY: &str = "__oqx_target";
50const REPO_TAG: &str = "$repo";
51
52/// The four scan targets.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum Target {
55    Docs,
56    Blocks,
57    Nodes,
58    Edges,
59}
60
61impl Target {
62    /// The root name: `docs` | `blocks` | `nodes` | `edges`.
63    #[must_use]
64    pub fn as_str(self) -> &'static str {
65        match self {
66            Target::Docs => "docs",
67            Target::Blocks => "blocks",
68            Target::Nodes => "nodes",
69            Target::Edges => "edges",
70        }
71    }
72
73    /// The target a root name denotes, if any.
74    #[must_use]
75    pub fn parse(s: &str) -> Option<Self> {
76        Some(match s {
77            "docs" => Target::Docs,
78            "blocks" => Target::Blocks,
79            "nodes" => Target::Nodes,
80            "edges" => Target::Edges,
81            _ => return None,
82        })
83    }
84}
85
86/// docs intrinsics whose BARE form is almost always a typo: a loud error
87/// ("did you mean the intrinsic").
88const RESERVED_DOC_BASENAMES: [&str; 5] = ["id", "path", "updated_at", "content_hash", "body"];
89
90/// A query phrase's embedding: the model whose cache to read and the vector
91/// as a float32 little-endian blob.
92#[derive(Clone, Debug, PartialEq)]
93pub struct SemanticVec {
94    pub model: String,
95    pub vec: Vec<u8>,
96}
97
98/// The store-backed context for one repo.
99pub struct StoreContext<'a> {
100    conn: &'a Connection,
101    repo_id: String,
102    semantic: HashMap<String, SemanticVec>,
103    /// A store failure inside [`DataContext::root`], the one read the
104    /// engine's seam cannot fail through (see the module doc).
105    root_failure: RefCell<Option<OqxError>>,
106    /// The rows a tier-3 plan produced, served as [`oqx::ROWS_ROOT`].
107    rows_root: Option<Vec<Value>>,
108}
109
110fn sql_value(v: ValueRef<'_>) -> Value {
111    match v {
112        ValueRef::Null => Value::Null,
113        ValueRef::Integer(i) => Value::Number(i as f64),
114        ValueRef::Real(f) => Value::Number(f),
115        ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
116        ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
117    }
118}
119
120/// A [`Value`] as a SQL parameter (`has_edge`'s destination, the planner's
121/// bound operands): booleans as 1/0 — how `json_extract` surfaces JSON
122/// booleans, so `attrs.b == true` compares against `1` — numbers as REAL
123/// (a JavaScript number binds as a double), absent as NULL.
124pub(crate) fn to_sql(v: &Value) -> SqlValue {
125    match v {
126        Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
127        Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
128        Value::Number(n) => SqlValue::Real(*n),
129        Value::Str(s) => SqlValue::Text(s.clone()),
130        Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
131    }
132}
133
134/// JavaScript `String(v)` of an argument.
135fn js_string(v: &Value) -> String {
136    v.to_string()
137}
138
139/// `String(args[0] ?? "")`.
140fn arg_or_empty(args: &[Value], i: usize) -> String {
141    match args.get(i) {
142        None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
143        Some(v) => js_string(v),
144    }
145}
146
147/// `JSON.parse` when a string, else the value (`null` → absent).
148fn parse_json(v: &Value) -> Value {
149    match v {
150        Value::Str(s) => {
151            serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
152        }
153        Value::Null | Value::Undefined => Value::Undefined,
154        other => other.clone(),
155    }
156}
157
158/// A store failure as the engine's eval error (the message the reference's
159/// raw exception would carry).
160fn sql_err(e: rusqlite::Error) -> OqxError {
161    OqxError::eval(format!("sqlite: {e}"))
162}
163
164/// The tag of a store row, if it is one.
165pub fn target_of(row: &Value) -> Option<Target> {
166    row.as_object()
167        .and_then(|o| o.get(TAG_KEY))
168        .and_then(Value::as_str)
169        .and_then(Target::parse)
170}
171
172fn is_repo_root(row: &Value) -> bool {
173    row.as_object()
174        .and_then(|o| o.get(TAG_KEY))
175        .and_then(Value::as_str)
176        == Some(REPO_TAG)
177}
178
179fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
180    row.as_object()
181        .and_then(|o| o.get(key))
182        .unwrap_or(&Value::Undefined)
183}
184
185fn col_str(row: &Value, key: &str) -> String {
186    match col(row, key) {
187        Value::Undefined | Value::Null => String::new(),
188        v => js_string(v),
189    }
190}
191
192/// Strip the hidden tag from a value tree (the wire form never carries it).
193#[must_use]
194pub fn strip_tags(v: Value) -> Value {
195    match v {
196        Value::Object(o) => Value::Object(
197            o.into_iter()
198                .filter(|(k, _)| k != TAG_KEY)
199                .map(|(k, x)| (k, strip_tags(x)))
200                .collect(),
201        ),
202        Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
203        other => other,
204    }
205}
206
207/// §1.4 rows as values (1.2): a store row that surfaces as a VALUE in a result
208/// tree — a nested `collect { }` / `first { }` / `single { }` with an empty
209/// projection, or a `values` item that is a row — renders as `{ id, path }`
210/// (the target's id column as a string; the owning document's path: a docs
211/// row's `path`, every other row's `__path` join column), never the store
212/// row. Everything else recurses, dropping the hidden tag as [`strip_tags`].
213#[must_use]
214pub fn render_row_values(v: Value) -> Value {
215    match v {
216        Value::Object(o) => {
217            let row = Value::Object(o);
218            if let Some(t) = target_of(&row) {
219                let (id_col, path_col) = match t {
220                    Target::Docs => ("doc_id", "path"),
221                    Target::Blocks => ("block_id", "__path"),
222                    Target::Nodes => ("node_id", "__path"),
223                    Target::Edges => ("edge_id", "__path"),
224                };
225                let mut out = Object::with_capacity(2);
226                out.insert("id", Value::Str(col_str(&row, id_col)));
227                out.insert("path", Value::Str(col_str(&row, path_col)));
228                return Value::Object(out);
229            }
230            let Value::Object(o) = row else {
231                unreachable!()
232            };
233            Value::Object(
234                o.into_iter()
235                    .filter(|(k, _)| k != TAG_KEY)
236                    .map(|(k, x)| (k, render_row_values(x)))
237                    .collect(),
238            )
239        }
240        Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
241        other => other,
242    }
243}
244
245impl<'a> StoreContext<'a> {
246    /// A context over `conn` scoped to `repo_id`, with the query phrases'
247    /// vectors for `semantic(...)` (empty when no provider ran).
248    #[must_use]
249    pub fn new(
250        conn: &'a Connection,
251        repo_id: &str,
252        semantic: HashMap<String, SemanticVec>,
253    ) -> Self {
254        Self {
255            conn,
256            repo_id: repo_id.to_owned(),
257            semantic,
258            root_failure: RefCell::new(None),
259            rows_root: None,
260        }
261    }
262
263    /// Serve `rows` — target-tagged store rows a plan produced — as the
264    /// [`oqx::ROWS_ROOT`] scan (the residual query's source). Every other
265    /// root and every relation, intrinsic and row function still hits the
266    /// store, so the residual sees exactly what a full scan would.
267    #[must_use]
268    pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
269        self.rows_root = Some(rows);
270        self
271    }
272
273    /// The store failure a root scan hit during the run, if any. Every other
274    /// read fails through the engine's channel (`get` / `call_method` return
275    /// `Err`); `root` has none, so it serves an empty scan and leaves the
276    /// failure here for the runner to report.
277    pub fn take_root_failure(&self) -> Option<OqxError> {
278        self.root_failure.borrow_mut().take()
279    }
280
281    // ---- SQL helpers ------------------------------------------------------------------
282
283    fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
284        fetch_rows(self.conn, sql, params).map_err(sql_err)
285    }
286
287    fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
288        Ok(self.all(sql, params)?.into_iter().next())
289    }
290
291    fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
292        Ok(self
293            .one(sql, params)?
294            .and_then(|o| o.values().next().cloned())
295            .unwrap_or(Value::Undefined))
296    }
297
298    fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
299        let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
300        stmt.exists(params_from_iter(params.iter()))
301            .map_err(sql_err)
302    }
303
304    fn tag_all(rows: Vec<Object>, t: Target) -> Value {
305        Value::Array(tag_rows(rows, t))
306    }
307
308    fn tag(row: Object, t: Target) -> Value {
309        tag_row(row, t)
310    }
311
312    fn repo_root(&self) -> Value {
313        let mut o = Object::with_capacity(1);
314        o.insert(TAG_KEY, Value::Str(REPO_TAG.to_owned()));
315        Value::Object(o)
316    }
317
318    // ---- roots (ordered for a stable (path, id) default) ------------------------------
319
320    fn root_scan(&self, t: Target) -> oqx::Result<Value> {
321        let repo = [SqlValue::Text(self.repo_id.clone())];
322        let sql = match t {
323            Target::Docs => {
324                "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
325            }
326            Target::Blocks => {
327                "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id
328                 WHERE b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
329                 ORDER BY d.path, b.block_id"
330            }
331            Target::Nodes => {
332                "SELECT n.*, d.path AS __path FROM nodes n JOIN docs d ON d.doc_id = n.doc_id
333                 WHERE n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
334            }
335            Target::Edges => {
336                "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
337                 WHERE e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
338                 ORDER BY d.path, e.edge_id"
339            }
340        };
341        Ok(Self::tag_all(self.all(sql, &repo)?, t))
342    }
343
344    // ---- properties ---------------------------------------------------------------------
345
346    /// A property row decoded to a plain scalar; a range-shaped string stays a
347    /// string (`range(prop)` is the opt-in).
348    fn decode_prop(r: &Object) -> Value {
349        let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
350        match get("type").as_str().unwrap_or("") {
351            "number" => get("val_num"),
352            "bool" => Value::Bool(get("val_bool").truthy()),
353            "null" => Value::Null,
354            "json" => parse_json(&get("val_json")),
355            _ => get("val_text"),
356        }
357    }
358
359    /// The scalar-vs-list rule: exactly one `card = scalar` row → the scalar;
360    /// otherwise the array; no row → the nested object under `key.`.
361    fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
362        let rows = match source {
363            Some(s) => self.all(
364                "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
365                &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
366            )?,
367            None => self.all(
368                "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
369                &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
370            )?,
371        };
372        if rows.is_empty() {
373            return self.doc_prop_object(doc_id, key, source);
374        }
375        if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
376            return Ok(Self::decode_prop(&rows[0]));
377        }
378        Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
379    }
380
381    /// The nested object rebuilt from flattened dotted keys under `prefix.`
382    /// (`Undefined` when none); leaves decoded.
383    fn doc_prop_object(
384        &self,
385        doc_id: &str,
386        prefix: &str,
387        source: Option<&str>,
388    ) -> oqx::Result<Value> {
389        let like = SqlValue::Text(format!("{prefix}.%"));
390        let rows = match source {
391            Some(s) => self.all(
392                "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
393                &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
394            )?,
395            None => self.all(
396                "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
397                &[SqlValue::Text(doc_id.to_owned()), like],
398            )?,
399        };
400        if rows.is_empty() {
401            return Ok(Value::Undefined);
402        }
403        let mut out = Object::new();
404        for r in &rows {
405            let key = r.get("key").and_then(Value::as_str).unwrap_or("");
406            let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
407                .split('.')
408                .collect();
409            set_nested(&mut out, &rest, Self::decode_prop(r));
410        }
411        Ok(Value::Object(out))
412    }
413
414    /// The `frontmatter` / `inline` bag as a plain object: one entry per
415    /// top-level key in key order, each valued by [`Self::doc_prop`].
416    fn doc_prop_bag(&self, doc_id: &str, source: &str) -> oqx::Result<Value> {
417        let keys = self.all(
418            "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
419            &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(source.to_owned())],
420        )?;
421        let mut out = Object::new();
422        for k in keys {
423            let key = k.get("key").and_then(Value::as_str).unwrap_or("");
424            let top = key.split('.').next().unwrap_or("");
425            if !out.contains_key(top) {
426                let v = self.doc_prop(doc_id, top, Some(source))?;
427                out.insert(top, v);
428            }
429        }
430        Ok(Value::Object(out))
431    }
432
433    // ---- structure ----------------------------------------------------------------------
434
435    /// The ordinal of a block's top-level ancestor (section ranges are in
436    /// top-level ordinals).
437    fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
438        let ordinal = col(block, "ordinal").clone();
439        if col(block, "parent_block").is_absent() {
440            return Ok(ordinal);
441        }
442        let ap = col_str(block, "ancestor_path");
443        let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
444            return Ok(ordinal);
445        };
446        let r = self.scalar(
447            "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
448            &[
449                SqlValue::Text(col_str(block, "doc_id")),
450                SqlValue::Text(first.to_owned()),
451            ],
452        )?;
453        Ok(if r.is_absent() { ordinal } else { r })
454    }
455
456    /// A document's live blocks in document order — pre-order over the
457    /// containment tree (children by `ordinal` under their parent; a row whose
458    /// parent is not live is a root) — tagged, with `__path` = `path`.
459    fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
460        let rows = self.all(
461            "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",
462            &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
463        )?;
464        let ids: Vec<String> = rows
465            .iter()
466            .map(|r| {
467                r.get("block_id")
468                    .and_then(Value::as_str)
469                    .unwrap_or("")
470                    .to_owned()
471            })
472            .collect();
473        let parent_index: Vec<Option<usize>> = rows
474            .iter()
475            .map(|r| {
476                r.get("parent_block")
477                    .and_then(Value::as_str)
478                    .and_then(|p| ids.iter().position(|id| id == p))
479            })
480            .collect();
481        let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
482        let mut roots = Vec::new();
483        for (i, p) in parent_index.iter().enumerate() {
484            match p {
485                Some(p) => children[*p].push(i),
486                None => roots.push(i),
487            }
488        }
489        fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
490            order.push(i);
491            for &c in &children[i] {
492                walk(c, children, order);
493            }
494        }
495        let mut order = Vec::with_capacity(rows.len());
496        for r in roots {
497            walk(r, &children, &mut order);
498        }
499        let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
500        Ok(order
501            .into_iter()
502            .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
503            .collect())
504    }
505
506    fn jattr(row: &Value, k: &str) -> Value {
507        match parse_json(col(row, "attrs")) {
508            Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
509            _ => Value::Undefined,
510        }
511    }
512
513    /// `Ok(None)` when `key` is not a relation of `t`.
514    fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
515        let path = || SqlValue::Text(col_str(row, "__path"));
516        let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
517        let doc_path = || SqlValue::Text(col_str(row, "path"));
518        let block_id = || SqlValue::Text(col_str(row, "block_id"));
519        let repo = || SqlValue::Text(self.repo_id.clone());
520        Ok(Some(match (t, key) {
521            (Target::Docs, "nodes") => {
522                // Document order: block-less nodes first, then by the owning
523                // block's pre-order rank, `span_start`, `node_id`.
524                let rows = self.all(
525                    "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
526                    &[doc_path(), doc_id()],
527                )?;
528                let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
529                let rank: HashMap<String, usize> = blocks
530                    .iter()
531                    .enumerate()
532                    .map(|(i, b)| (col_str(b, "block_id"), i))
533                    .collect();
534                let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
535                    .into_iter()
536                    .map(|r| {
537                        let block = r.get("block_id").and_then(Value::as_str);
538                        let (has_block, rk) = match block {
539                            None => (0, 0),
540                            Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
541                        };
542                        let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
543                        let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
544                        ((has_block, rk, span, id), r)
545                    })
546                    .collect();
547                keyed.sort_by(|a, b| {
548                    a.0.0
549                        .cmp(&b.0.0)
550                        .then(a.0.1.cmp(&b.0.1))
551                        .then(a.0.2.total_cmp(&b.0.2))
552                        .then(a.0.3.cmp(&b.0.3))
553                });
554                Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
555            }
556            (Target::Docs, "blocks") => {
557                Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
558            }
559            (Target::Docs, "out") => Self::tag_all(
560                self.all(
561                    "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.dst_node = d2.doc_id
562                     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",
563                    &[doc_id(), repo()],
564                )?,
565                Target::Docs,
566            ),
567            (Target::Docs, "in") => Self::tag_all(
568                self.all(
569                    "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.src_doc = d2.doc_id
570                     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",
571                    &[doc_id(), repo()],
572                )?,
573                Target::Docs,
574            ),
575            (Target::Docs, "out_edges") => Self::tag_all(
576                self.all(
577                    "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",
578                    &[doc_path(), doc_id()],
579                )?,
580                Target::Edges,
581            ),
582            (Target::Docs, "in_edges") => Self::tag_all(
583                self.all(
584                    "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
585                     WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
586                    &[doc_id()],
587                )?,
588                Target::Edges,
589            ),
590            (Target::Blocks, "children") => Self::tag_all(
591                self.all(
592                    "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",
593                    &[path(), block_id()],
594                )?,
595                Target::Blocks,
596            ),
597            (Target::Blocks, "nodes") => Self::tag_all(
598                self.all(
599                    "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
600                    &[path(), block_id()],
601                )?,
602                Target::Nodes,
603            ),
604            (Target::Blocks, "out_edges") => Self::tag_all(
605                self.all(
606                    "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",
607                    &[path(), block_id()],
608                )?,
609                Target::Edges,
610            ),
611            (Target::Blocks, "section") => {
612                let top = to_sql(&self.top_ordinal(row)?);
613                Self::tag_all(
614                    self.all(
615                        "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
616                           AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
617                         ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
618                        &[path(), doc_id(), top.clone(), top],
619                    )?,
620                    Target::Nodes,
621                )
622            }
623            (Target::Nodes, "blocks") => {
624                let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
625                if f.is_absent() || l.is_absent() {
626                    return Ok(Some(Value::Array(Vec::new())));
627                }
628                let (f, l) = (
629                    f.as_f64().unwrap_or(f64::NAN),
630                    l.as_f64().unwrap_or(f64::NAN),
631                );
632                let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
633                let mut kept: Vec<Value> = Vec::new();
634                for b in rows {
635                    let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
636                    if t >= f && t <= l {
637                        kept.push(b);
638                    }
639                }
640                Value::Array(kept)
641            }
642            (Target::Nodes, "subsections") => {
643                let (f, l, lvl) = (
644                    Self::jattr(row, "first_ordinal"),
645                    Self::jattr(row, "last_ordinal"),
646                    Self::jattr(row, "level"),
647                );
648                if f.is_absent() {
649                    return Ok(Some(Value::Array(Vec::new())));
650                }
651                Self::tag_all(
652                    self.all(
653                        "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
654                           AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
655                           AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
656                        &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
657                    )?,
658                    Target::Nodes,
659                )
660            }
661            (Target::Nodes, "children") => {
662                let (f, l, lvl) = (
663                    Self::jattr(row, "first_ordinal"),
664                    Self::jattr(row, "last_ordinal"),
665                    Self::jattr(row, "level"),
666                );
667                if f.is_absent() {
668                    return Ok(Some(Value::Array(Vec::new())));
669                }
670                Self::tag_all(
671                    self.all(
672                        "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
673                           AND json_extract(i.attrs,'$.level') > ?3
674                           AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
675                           AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
676                             AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
677                             AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
678                             AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
679                         ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
680                        &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
681                    )?,
682                    Target::Nodes,
683                )
684            }
685            _ => return Ok(None),
686        }))
687    }
688
689    fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
690        let id = match col(row, "doc_id") {
691            Value::Undefined | Value::Null => col(row, "src_doc").clone(),
692            v => v.clone(),
693        };
694        Ok(self
695            .one("SELECT * FROM docs WHERE doc_id = ?1", &[to_sql(&id)])?
696            .map_or(Value::Undefined, |o| Self::tag(o, Target::Docs)))
697    }
698
699    fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
700        let id = col(row, "block_id");
701        if !id.truthy() {
702            return Ok(Value::Undefined);
703        }
704        Ok(self
705            .one(
706                "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
707                &[to_sql(id)],
708            )?
709            .map_or(Value::Undefined, |o| Self::tag(o, Target::Blocks)))
710    }
711
712    // ---- intrinsics ---------------------------------------------------------------------
713
714    fn null_if_absent(v: Value) -> Value {
715        if v.is_absent() { Value::Null } else { v }
716    }
717
718    fn intrinsic(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
719        if name == "$self" {
720            return Ok(row.clone());
721        }
722        let c = |k: &str| col(row, k).clone();
723        Ok(match (t, name) {
724            (Target::Docs, "$id") => c("doc_id"),
725            (Target::Docs, "$path") => c("path"),
726            (Target::Docs, "$content_hash") => Self::null_if_absent(c("file_hash")),
727            (Target::Docs, "$updated_at") => Self::null_if_absent(self.scalar(
728                "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
729                &[to_sql(&c("current_rev"))],
730            )?),
731            (Target::Docs, "$body") => {
732                match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
733                    Ok(Some(s)) => Value::Str(s),
734                    Ok(None) => Value::Null,
735                    Err(e) => return Err(OqxError::eval(e.to_string())),
736                }
737            }
738            (Target::Docs, "$title") => {
739                Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
740            }
741            (Target::Docs, "$tags") => {
742                Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$tags", Some("computed"))?)
743            }
744            (Target::Blocks, "$id") => c("block_id"),
745            (Target::Blocks, "$doc") => c("doc_id"),
746            (Target::Blocks, "$path") => c("__path"),
747            (Target::Blocks, "$ordinal") => c("ordinal"),
748            (Target::Blocks, "$depth") => c("depth"),
749            (Target::Blocks, "$body") => c("text"),
750            (Target::Blocks, "$content_hash") => Self::null_if_absent(c("raw_hash")),
751            (Target::Blocks, "$updated_at") => Self::null_if_absent(self.scalar(
752                "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
753                &[to_sql(&c("block_id"))],
754            )?),
755            (Target::Nodes, "$id" | "$node_id") => c("node_id"),
756            (Target::Nodes, "$doc_id") => c("doc_id"),
757            (Target::Nodes, "$block_id") => c("block_id"),
758            (Target::Nodes, "$path") => c("__path"),
759            (Target::Edges, "$id") => c("edge_id"),
760            (Target::Edges, "$src") => c("src_doc"),
761            (Target::Edges, "$dst") => c("dst_node"),
762            (Target::Edges, "$src_block") => c("src_block"),
763            (Target::Edges, "$via") => c("via_node"),
764            (Target::Edges, "$from_commit") => c("from_commit"),
765            (Target::Edges, "$path") => c("__path"),
766            (Target::Edges, "$dst_path") => Self::null_if_absent(self.scalar(
767                "SELECT path FROM docs WHERE doc_id = ?1",
768                &[to_sql(&c("dst_node"))],
769            )?),
770            (Target::Edges, "$dst_uri") => Self::null_if_absent(self.scalar(
771                "SELECT uri FROM external_nodes WHERE node_id = ?1",
772                &[to_sql(&c("dst_node"))],
773            )?),
774            _ => Value::Undefined,
775        })
776    }
777
778    // ---- row functions (methods on `$self`) ----------------------------------------------
779
780    fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
781        Some(Err(OqxError::eval(msg)))
782    }
783
784    fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
785        (t != want).then(|| {
786            Err(OqxError::eval(format!(
787                "{name}() is only available on the {} target",
788                want.as_str()
789            )))
790        })
791    }
792
793    fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
794        r.map(Value::Bool)
795    }
796
797    fn row_method(
798        &self,
799        name: &str,
800        row: &Value,
801        t: Target,
802        args: &[Value],
803    ) -> Option<oqx::Result<Value>> {
804        let c = |k: &str| col(row, k).clone();
805        match name {
806            "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
807            "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
808            "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
809                Some(Self::sql_result(self.exists(
810                    "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
811                    &[to_sql(&c("block_id"))],
812                )))
813            }),
814            "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
815                Some(self.scalar(
816                    "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
817                    &[to_sql(&c("block_id"))],
818                ))
819            }),
820            "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
821                Some(
822                    self.scalar(
823                        "SELECT type FROM blocks WHERE block_id = ?1",
824                        &[to_sql(&c("parent_block"))],
825                    )
826                    .map(Self::null_if_absent),
827                )
828            }),
829            "has_edge" => {
830                let pred = js_string(args.first().unwrap_or(&Value::Undefined));
831                let (src_col, src_val) = if t == Target::Blocks {
832                    ("src_block", c("block_id"))
833                } else {
834                    ("src_doc", c("doc_id"))
835                };
836                let r = if args.len() >= 2 {
837                    self.exists(
838                        &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
839                        &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
840                    )
841                } else {
842                    self.exists(
843                        &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
844                        &[to_sql(&src_val), SqlValue::Text(pred)],
845                    )
846                };
847                Some(Self::sql_result(r))
848            }
849            "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
850                let target = js_string(args.first().unwrap_or(&Value::Undefined));
851                let ap = col_str(row, "ancestor_path");
852                Some(Ok(Value::Bool(
853                    ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
854                )))
855            }),
856            "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
857                let text = js_string(args.first().unwrap_or(&Value::Undefined));
858                let top = match self.top_ordinal(row) {
859                    Ok(v) => to_sql(&v),
860                    Err(e) => return Some(Err(e)),
861                };
862                Some(Self::sql_result(self.exists(
863                    "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
864                     WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
865                    &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
866                )))
867            }),
868            "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
869                let target = js_string(args.first().unwrap_or(&Value::Undefined));
870                if target.starts_with("d_") {
871                    return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
872                }
873                if target.contains('*') {
874                    let like = glob_to_like(&target, false);
875                    return Some(Self::sql_result(self.exists(
876                        "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
877                        &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
878                    )));
879                }
880                Some(Ok(Value::Bool(col_str(row, "__path") == target)))
881            }),
882            "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
883                let kind = js_string(args.first().unwrap_or(&Value::Undefined));
884                let ap: Vec<String> = col_str(row, "ancestor_path")
885                    .split('/')
886                    .filter(|s| !s.is_empty())
887                    .map(str::to_owned)
888                    .collect();
889                if ap.is_empty() {
890                    return Some(Ok(Value::Bool(false)));
891                }
892                let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
893                let placeholders = placeholders.join(",");
894                let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
895                let n = params.len();
896                params.push(SqlValue::Text(kind));
897                let r = match args.get(1) {
898                    Some(v) if !v.is_absent() => {
899                        let nm = js_string(v);
900                        params.push(SqlValue::Text(nm.clone()));
901                        params.push(SqlValue::Text(nm));
902                        self.exists(
903                            &format!(
904                                "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
905                                n + 1,
906                                n + 2,
907                                n + 3
908                            ),
909                            &params,
910                        )
911                    }
912                    _ => self.exists(
913                        &format!(
914                            "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
915                            n + 1
916                        ),
917                        &params,
918                    ),
919                };
920                Some(Self::sql_result(r))
921            }),
922            "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
923                Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
924            }),
925            "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
926                Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
927            }),
928            _ => None,
929        }
930    }
931
932    fn key_path(row: &Value, path: &str, kind: &str) -> Value {
933        let key = if kind == "json" {
934            let mut p = path;
935            p = p.strip_prefix('#').unwrap_or(p);
936            p = p.strip_prefix('/').unwrap_or(p);
937            p.split('/').collect::<Vec<_>>().join(".")
938        } else {
939            path.to_owned()
940        };
941        let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
942        if !col_str(row, "type").starts_with(&format!("{kind}:")) {
943            return Value::Bool(false);
944        }
945        let k = Self::jattr(row, "key");
946        Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
947    }
948
949    fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
950        if t == Target::Edges {
951            return Err(OqxError::eval(
952                "text(...) is not available on the edges target",
953            ));
954        }
955        let m = sanitize_fts_query(terms);
956        if m.is_empty() {
957            return Ok(Value::Bool(false));
958        }
959        let r = match t {
960            Target::Docs => self.exists(
961                "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",
962                &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
963            ),
964            Target::Nodes => self.exists(
965                "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
966                &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
967            ),
968            _ => self.exists(
969                "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
970                &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
971            ),
972        };
973        Self::sql_result(r)
974    }
975
976    fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
977        if matches!(t, Target::Nodes | Target::Edges) {
978            return Err(OqxError::eval(
979                "semantic(...) is available on the docs and blocks targets",
980            ));
981        }
982        let Some(resolved) = self.semantic.get(phrase) else {
983            return Err(OqxError::eval(format!(
984                "semantic({}) needs an embedding provider; none is configured for this query",
985                serde_json::Value::String(phrase.to_owned())
986            )));
987        };
988        let vec: oqx::Result<Option<Vec<u8>>> = match t {
989            Target::Docs => self
990                .conn
991                .query_row(
992                    "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
993                    rusqlite::params![col_str(row, "doc_id"), resolved.model],
994                    |r| r.get(0),
995                )
996                .optional()
997                .map_err(sql_err),
998            // The row for the block's current `(raw_hash, ctx_hash)` only
999            // (`spec/search` §3, 1.1): a stale context row is never read.
1000            _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1001                .map_err(|e| OqxError::eval(e.to_string())),
1002        };
1003        match vec? {
1004            Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1005            None => Ok(Value::Null),
1006        }
1007    }
1008}
1009
1010/// Run `sql` and read every row as a column object (blobs as hex, integers
1011/// and reals as numbers) — the one shape a store row ever has in a query.
1012pub(crate) fn fetch_rows(
1013    conn: &Connection,
1014    sql: &str,
1015    params: &[SqlValue],
1016) -> rusqlite::Result<Vec<Object>> {
1017    let mut stmt = conn.prepare_cached(sql)?;
1018    let names: Vec<String> = stmt
1019        .column_names()
1020        .iter()
1021        .map(|s| (*s).to_owned())
1022        .collect();
1023    let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1024        let mut o = Object::with_capacity(names.len());
1025        for (i, name) in names.iter().enumerate() {
1026            o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1027        }
1028        Ok(o)
1029    })?;
1030    rows.collect()
1031}
1032
1033/// Tag a store row with its target so the context resolves it (the
1034/// reference's `tagRows`; the planner hands produced rows back this way).
1035pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1036    row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1037    Value::Object(row)
1038}
1039
1040/// [`tag_row`] over a result set.
1041pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1042    rows.into_iter().map(|r| tag_row(r, t)).collect()
1043}
1044
1045/// `cur[seg] = {}` down the path, then the leaf (a scalar in the way is
1046/// replaced by an object; an existing key keeps its position).
1047fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1048    let Some((first, rest)) = path.split_first() else {
1049        return;
1050    };
1051    if rest.is_empty() {
1052        out.insert(*first, leaf);
1053        return;
1054    }
1055    let mut child = match out.get(first) {
1056        Some(Value::Object(o)) => o.clone(),
1057        _ => Object::new(),
1058    };
1059    set_nested(&mut child, rest, leaf);
1060    out.insert(*first, Value::Object(child));
1061}
1062
1063/// A `*` glob as a `LIKE` pattern with `ESCAPE '\'`; `escape_backslash`
1064/// also escapes `\` (the list surfaces do, `within` does not).
1065#[must_use]
1066pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1067    let mut out = String::with_capacity(glob.len() + 4);
1068    for ch in glob.chars() {
1069        match ch {
1070            '%' | '_' => {
1071                out.push('\\');
1072                out.push(ch);
1073            }
1074            '\\' if escape_backslash => out.push_str("\\\\"),
1075            '*' => out.push('%'),
1076            c => out.push(c),
1077        }
1078    }
1079    out
1080}
1081
1082impl DataContext for StoreContext<'_> {
1083    fn root(&self, name: &str) -> Value {
1084        if let Some(rows) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1085            return Value::Array(rows.clone());
1086        }
1087        if name == "$repo" {
1088            return self.repo_root();
1089        }
1090        let Some(t) = Target::parse(name) else {
1091            return Value::Undefined;
1092        };
1093        // No error channel here: a failed scan is served empty and reported
1094        // by the runner (see `take_root_failure`).
1095        match self.root_scan(t) {
1096            Ok(rows) => rows,
1097            Err(e) => {
1098                let mut slot = self.root_failure.borrow_mut();
1099                if slot.is_none() {
1100                    *slot = Some(e);
1101                }
1102                Value::Array(Vec::new())
1103            }
1104        }
1105    }
1106
1107    fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1108        if row.is_absent() {
1109            return Ok(Value::Undefined);
1110        }
1111        // `$repo` is an intrinsic of EVERY scope, so a correlated subquery at any
1112        // depth reaches the repository root without scope climbing.
1113        if key == "$repo" {
1114            return Ok(self.repo_root());
1115        }
1116        if is_repo_root(row) {
1117            if key == "$id" {
1118                return Ok(Value::Str(self.repo_id.clone()));
1119            }
1120            return match Target::parse(key) {
1121                Some(t) => self.root_scan(t),
1122                None => Ok(Value::Undefined),
1123            };
1124        }
1125        let Some(t) = target_of(row) else {
1126            // A plain value (parsed attrs, a property bag, a lifted element).
1127            return Ok(oqx::DefaultContext::read(row, key));
1128        };
1129        if key.starts_with('$') {
1130            return self.intrinsic(row, t, key);
1131        }
1132        // self-alias namespaces
1133        match (t, key) {
1134            (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1135                return Ok(row.clone());
1136            }
1137            (_, "doc") => return self.owning_doc(row),
1138            (Target::Nodes, "block") => return self.owning_block(row),
1139            _ => {}
1140        }
1141        if let Some(v) = self.relation(row, t, key)? {
1142            return Ok(v);
1143        }
1144        let c = |k: &str| col(row, k).clone();
1145        Ok(match t {
1146            Target::Docs => {
1147                if key == "format" {
1148                    return Ok(c("format"));
1149                }
1150                let doc_id = col_str(row, "doc_id");
1151                if key == "frontmatter" || key == "inline" {
1152                    return self.doc_prop_bag(&doc_id, key);
1153                }
1154                if RESERVED_DOC_BASENAMES.contains(&key) {
1155                    // The reference's `FilterInvalid` thrown from `get`; the
1156                    // runner maps this eval error to `filter_invalid` with
1157                    // the same message.
1158                    return Err(OqxError::eval(format!(
1159                        "bare '{key}' reads a frontmatter key; did you mean the intrinsic ${key}? (use frontmatter.{key} to force the property)"
1160                    )));
1161                }
1162                return self.doc_prop(&doc_id, key, None);
1163            }
1164            Target::Blocks => match key {
1165                "type" => c("type"),
1166                "text" => c("text"),
1167                "attrs" => parse_json(&c("attrs")),
1168                _ => Self::jattr(row, key),
1169            },
1170            Target::Nodes => match key {
1171                "kind" => c("kind"),
1172                "name" => c("name"),
1173                "value" => c("value"),
1174                "attrs" => parse_json(&c("attrs")),
1175                _ => Self::jattr(row, key),
1176            },
1177            Target::Edges => match key {
1178                "predicate" | "provenance" | "dst_kind" | "anchor" | "src_field" => c(key),
1179                _ => Value::Undefined,
1180            },
1181        })
1182    }
1183
1184    fn to_rows(&self, value: &Value) -> Vec<Value> {
1185        match value {
1186            Value::Undefined | Value::Null => Vec::new(),
1187            Value::Array(a) => a.clone(),
1188            other => vec![other.clone()],
1189        }
1190    }
1191
1192    fn identity(&self, row: &Value) -> Value {
1193        match target_of(row) {
1194            Some(Target::Docs) => col(row, "doc_id").clone(),
1195            Some(Target::Blocks) => col(row, "block_id").clone(),
1196            Some(Target::Nodes) => col(row, "node_id").clone(),
1197            Some(Target::Edges) => col(row, "edge_id").clone(),
1198            None => row.clone(),
1199        }
1200    }
1201
1202    fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
1203        if name == "range" {
1204            let x = args.first().unwrap_or(&Value::Undefined);
1205            return Some(Ok(match x {
1206                Value::Range(_) => x.clone(),
1207                Value::Str(s) => match omgbase_properties::detect_range(s) {
1208                    Some(r) => {
1209                        let b = |b: &Bound| match b {
1210                            Bound::Open => Value::Undefined,
1211                            Bound::Num(n) => Value::Number(*n),
1212                            Bound::Iso(s) => Value::Str(s.clone()),
1213                        };
1214                        Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
1215                    }
1216                    None => Value::Null,
1217                },
1218                _ => Value::Null,
1219            }));
1220        }
1221        builtin_function(name, args)
1222    }
1223
1224    fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1225        if let Some(t) = target_of(recv) {
1226            if let Some(r) = self.row_method(name, recv, t, args) {
1227                return Some(r);
1228            }
1229        } else if matches!(
1230            name,
1231            "text"
1232                | "semantic"
1233                | "under"
1234                | "under_heading"
1235                | "within"
1236                | "under_kind"
1237                | "yaml_path"
1238                | "json_pointer"
1239                | "has_edge"
1240                | "has_anchor"
1241                | "child_count"
1242                | "parent_type"
1243        ) {
1244            return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/edges row"));
1245        }
1246        builtin_method_with(RegexDialect::Oqx, name, recv, args)
1247    }
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252    use super::*;
1253
1254    #[test]
1255    fn glob_to_like_escapes() {
1256        assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
1257        assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
1258        assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
1259    }
1260
1261    #[test]
1262    fn rows_surfacing_as_values_render_id_and_path() {
1263        // §1.4 (1.2): a tagged store row anywhere in a value tree is
1264        // `{ id, path }`; untagged records keep their keys, minus the tag.
1265        let mut node = Object::new();
1266        node.insert("node_id", Value::Str("n_1".into()));
1267        node.insert("attrs", Value::Str("{\"checked\":true}".into()));
1268        node.insert("__path", Value::Str("a.md".into()));
1269        let mut doc = Object::new();
1270        doc.insert("doc_id", Value::Str("d_0".into()));
1271        doc.insert("path", Value::Str("a.md".into()));
1272        doc.insert("blob", Value::Str("ff".into()));
1273        let mut record = Object::new();
1274        record.insert(TAG_KEY, Value::Str("junk".into()));
1275        record.insert(
1276            "tasks",
1277            Value::Array(vec![
1278                tag_row(node, Target::Nodes),
1279                tag_row(doc, Target::Docs),
1280            ]),
1281        );
1282        let out = render_row_values(Value::Object(record));
1283        let o = out.as_object().unwrap();
1284        assert!(o.get(TAG_KEY).is_none());
1285        let tasks = o.get("tasks").unwrap().as_array().unwrap();
1286        let keys = |v: &Value| -> Vec<String> {
1287            v.as_object()
1288                .unwrap()
1289                .iter()
1290                .map(|(k, _)| k.to_owned())
1291                .collect()
1292        };
1293        assert_eq!(keys(&tasks[0]), ["id", "path"]);
1294        assert_eq!(
1295            tasks[0].as_object().unwrap().get("id"),
1296            Some(&Value::Str("n_1".into()))
1297        );
1298        assert_eq!(
1299            tasks[0].as_object().unwrap().get("path"),
1300            Some(&Value::Str("a.md".into()))
1301        );
1302        assert_eq!(keys(&tasks[1]), ["id", "path"]);
1303        assert_eq!(
1304            tasks[1].as_object().unwrap().get("id"),
1305            Some(&Value::Str("d_0".into()))
1306        );
1307        // An id column that is not a string still renders as its string form.
1308        let mut edge = Object::new();
1309        edge.insert("edge_id", Value::Number(7.0));
1310        let e = render_row_values(tag_row(edge, Target::Edges));
1311        assert_eq!(
1312            e.as_object().unwrap().get("id"),
1313            Some(&Value::Str("7".into()))
1314        );
1315        assert_eq!(
1316            e.as_object().unwrap().get("path"),
1317            Some(&Value::Str(String::new()))
1318        );
1319    }
1320
1321    #[test]
1322    fn nested_property_objects_rebuild() {
1323        let mut o = Object::new();
1324        set_nested(&mut o, &["a", "b"], Value::Number(1.0));
1325        set_nested(&mut o, &["a", "c"], Value::Number(2.0));
1326        set_nested(&mut o, &["d"], Value::Str("x".into()));
1327        let a = o.get("a").unwrap().as_object().unwrap();
1328        assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
1329        assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
1330        assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
1331        // A scalar in the way is replaced by an object.
1332        set_nested(&mut o, &["d", "e"], Value::Bool(true));
1333        assert!(o.get("d").unwrap().as_object().is_some());
1334    }
1335
1336    #[test]
1337    fn json_and_sql_bridges() {
1338        assert_eq!(
1339            parse_json(&Value::Str("{\"a\":1}".into()))
1340                .as_object()
1341                .unwrap()
1342                .get("a"),
1343            Some(&Value::Number(1.0))
1344        );
1345        assert_eq!(
1346            parse_json(&Value::Str("nope".into())),
1347            Value::Str("nope".into())
1348        );
1349        assert_eq!(parse_json(&Value::Null), Value::Undefined);
1350        assert_eq!(arg_or_empty(&[], 0), "");
1351        assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
1352    }
1353}