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