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