1use std::cell::{Cell, RefCell};
39use std::collections::HashMap;
40use std::rc::Rc;
41
42use omgbase_properties::Bound;
43use omgbase_search::{cosine_bytes, sanitize_fts_query};
44use oqx::semantics::{builtin_function, builtin_method_with, make_range, string_form};
45use oqx::{
46 CompiledRegex, DataContext, Object, OqxError, RegexDialect, RowIndex, Value, compile_regex,
47};
48use rusqlite::types::{Value as SqlValue, ValueRef};
49use rusqlite::{Connection, OptionalExtension, params_from_iter};
50
51pub const TAG_KEY: &str = "__oqx_target";
53const REPO_TAG: &str = "$repo";
54pub const SCAN_KEY: &str = "__oqx_scan";
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
67pub enum Target {
68 Docs,
69 Blocks,
70 Nodes,
71 Edges,
72}
73
74impl Target {
75 #[must_use]
77 pub fn as_str(self) -> &'static str {
78 match self {
79 Target::Docs => "docs",
80 Target::Blocks => "blocks",
81 Target::Nodes => "nodes",
82 Target::Edges => "edges",
83 }
84 }
85
86 #[must_use]
88 pub fn parse(s: &str) -> Option<Self> {
89 Some(match s {
90 "docs" => Target::Docs,
91 "blocks" => Target::Blocks,
92 "nodes" => Target::Nodes,
93 "edges" => Target::Edges,
94 _ => return None,
95 })
96 }
97}
98
99const RESERVED_DOC_BASENAMES: [&str; 5] = ["id", "path", "updated_at", "content_hash", "body"];
102
103#[derive(Clone, Debug, PartialEq)]
106pub struct SemanticVec {
107 pub model: String,
108 pub vec: Vec<u8>,
109}
110
111pub struct StoreContext<'a> {
113 conn: &'a Connection,
114 repo_id: String,
115 semantic: HashMap<String, SemanticVec>,
116 root_failure: RefCell<Option<OqxError>>,
119 rows_root: Option<RowsRoot>,
121 regexes: RefCell<HashMap<(String, Option<String>), CompiledRegex>>,
124 scans: RefCell<HashMap<Target, Rc<Vec<Value>>>>,
128 scans_run: Cell<usize>,
130}
131
132struct RowsRoot {
137 rows: RefCell<Option<Vec<Value>>>,
138 once: bool,
139}
140
141fn sql_value(v: ValueRef<'_>) -> Value {
142 match v {
143 ValueRef::Null => Value::Null,
144 ValueRef::Integer(i) => Value::Number(i as f64),
145 ValueRef::Real(f) => Value::Number(f),
146 ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
147 ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
148 }
149}
150
151pub(crate) fn to_sql(v: &Value) -> SqlValue {
156 match v {
157 Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
158 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
159 Value::Number(n) => SqlValue::Real(*n),
160 Value::Str(s) => SqlValue::Text(s.clone()),
161 Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
162 }
163}
164
165fn js_string(v: &Value) -> String {
167 v.to_string()
168}
169
170fn arg_or_empty(args: &[Value], i: usize) -> String {
172 match args.get(i) {
173 None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
174 Some(v) => js_string(v),
175 }
176}
177
178fn parse_json(v: &Value) -> Value {
180 match v {
181 Value::Str(s) => {
182 serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
183 }
184 Value::Null | Value::Undefined => Value::Undefined,
185 other => other.clone(),
186 }
187}
188
189fn sql_err(e: rusqlite::Error) -> OqxError {
192 OqxError::eval(format!("sqlite: {e}"))
193}
194
195pub fn target_of(row: &Value) -> Option<Target> {
197 row.as_object()
198 .and_then(|o| o.get(TAG_KEY))
199 .and_then(Value::as_str)
200 .and_then(Target::parse)
201}
202
203#[must_use]
205pub fn scan_of(v: &Value) -> Option<Target> {
206 v.as_object()
207 .and_then(|o| o.get(SCAN_KEY))
208 .and_then(Value::as_str)
209 .and_then(Target::parse)
210}
211
212fn scan_marker(t: Target) -> Value {
213 let mut o = Object::with_capacity(1);
214 o.insert(SCAN_KEY, Value::Str(t.as_str().to_owned()));
215 Value::Object(o)
216}
217
218fn is_repo_root(row: &Value) -> bool {
219 row.as_object()
220 .and_then(|o| o.get(TAG_KEY))
221 .and_then(Value::as_str)
222 == Some(REPO_TAG)
223}
224
225fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
226 row.as_object()
227 .and_then(|o| o.get(key))
228 .unwrap_or(&Value::Undefined)
229}
230
231fn col_str(row: &Value, key: &str) -> String {
232 match col(row, key) {
233 Value::Undefined | Value::Null => String::new(),
234 v => js_string(v),
235 }
236}
237
238#[must_use]
240pub fn strip_tags(v: Value) -> Value {
241 match v {
242 Value::Object(o) => Value::Object(
243 o.into_iter()
244 .filter(|(k, _)| k != TAG_KEY)
245 .map(|(k, x)| (k, strip_tags(x)))
246 .collect(),
247 ),
248 Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
249 other => other,
250 }
251}
252
253#[must_use]
260pub fn render_row_values(v: Value) -> Value {
261 match v {
262 Value::Object(o) => {
263 let row = Value::Object(o);
264 if let Some(t) = target_of(&row) {
265 let (id_col, path_col) = match t {
266 Target::Docs => ("doc_id", "path"),
267 Target::Blocks => ("block_id", "__path"),
268 Target::Nodes => ("node_id", "__path"),
269 Target::Edges => ("edge_id", "__path"),
270 };
271 let mut out = Object::with_capacity(2);
272 out.insert("id", Value::Str(col_str(&row, id_col)));
273 out.insert("path", Value::Str(col_str(&row, path_col)));
274 return Value::Object(out);
275 }
276 let Value::Object(o) = row else {
277 unreachable!()
278 };
279 Value::Object(
280 o.into_iter()
281 .filter(|(k, _)| k != TAG_KEY)
282 .map(|(k, x)| (k, render_row_values(x)))
283 .collect(),
284 )
285 }
286 Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
287 other => other,
288 }
289}
290
291impl<'a> StoreContext<'a> {
292 #[must_use]
295 pub fn new(
296 conn: &'a Connection,
297 repo_id: &str,
298 semantic: HashMap<String, SemanticVec>,
299 ) -> Self {
300 Self {
301 conn,
302 repo_id: repo_id.to_owned(),
303 semantic,
304 root_failure: RefCell::new(None),
305 rows_root: None,
306 regexes: RefCell::new(HashMap::new()),
307 scans: RefCell::new(HashMap::new()),
308 scans_run: Cell::new(0),
309 }
310 }
311
312 #[must_use]
314 pub fn repo_id(&self) -> &str {
315 &self.repo_id
316 }
317
318 #[must_use]
322 pub fn scans_run(&self) -> usize {
323 self.scans_run.get()
324 }
325
326 pub(crate) fn scan_rows(&self, t: Target) -> oqx::Result<Rc<Vec<Value>>> {
328 if let Some(rows) = self.scans.borrow().get(&t) {
329 return Ok(Rc::clone(rows));
330 }
331 self.scans_run.set(self.scans_run.get() + 1);
332 let rows = match self.root_scan(t)? {
333 Value::Array(rows) => Rc::new(rows),
334 _ => Rc::new(Vec::new()),
335 };
336 self.scans.borrow_mut().insert(t, Rc::clone(&rows));
337 Ok(rows)
338 }
339
340 pub(crate) fn probe_rows(
342 &self,
343 t: Target,
344 sql: &str,
345 params: &[SqlValue],
346 ) -> oqx::Result<Vec<Value>> {
347 Ok(tag_rows(self.all(sql, params)?, t))
348 }
349
350 fn expand_scan(&self, t: Target) -> Vec<Value> {
353 match self.scan_rows(t) {
354 Ok(rows) => rows.as_ref().clone(),
355 Err(e) => {
356 let mut slot = self.root_failure.borrow_mut();
357 if slot.is_none() {
358 *slot = Some(e);
359 }
360 Vec::new()
361 }
362 }
363 }
364
365 #[must_use]
370 pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
371 self.rows_root = Some(RowsRoot {
372 rows: RefCell::new(Some(rows)),
373 once: false,
374 });
375 self
376 }
377
378 #[must_use]
386 pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
387 self.rows_root = Some(RowsRoot {
388 rows: RefCell::new(Some(rows)),
389 once: true,
390 });
391 self
392 }
393
394 pub fn take_root_failure(&self) -> Option<OqxError> {
399 self.root_failure.borrow_mut().take()
400 }
401
402 fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
405 fetch_rows(self.conn, sql, params).map_err(sql_err)
406 }
407
408 fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
409 Ok(self.all(sql, params)?.into_iter().next())
410 }
411
412 fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
413 Ok(self
414 .one(sql, params)?
415 .and_then(|o| o.values().next().cloned())
416 .unwrap_or(Value::Undefined))
417 }
418
419 fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
420 let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
421 stmt.exists(params_from_iter(params.iter()))
422 .map_err(sql_err)
423 }
424
425 fn tag_all(rows: Vec<Object>, t: Target) -> Value {
426 Value::Array(tag_rows(rows, t))
427 }
428
429 fn tag(row: Object, t: Target) -> Value {
430 tag_row(row, t)
431 }
432
433 fn repo_root(&self) -> Value {
434 let mut o = Object::with_capacity(1);
435 o.insert(TAG_KEY, Value::Str(REPO_TAG.to_owned()));
436 Value::Object(o)
437 }
438
439 fn root_scan(&self, t: Target) -> oqx::Result<Value> {
452 let repo = [SqlValue::Text(self.repo_id.clone())];
453 let sql = match t {
454 Target::Docs => {
455 "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
456 }
457 Target::Blocks => {
458 "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
459 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
460 ORDER BY d.path, b.block_id"
461 }
462 Target::Nodes => {
463 "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
464 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
465 }
466 Target::Edges => {
467 "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
468 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
469 ORDER BY d.path, e.edge_id"
470 }
471 };
472 Ok(Self::tag_all(self.all(sql, &repo)?, t))
473 }
474
475 fn decode_prop(r: &Object) -> Value {
480 let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
481 match get("type").as_str().unwrap_or("") {
482 "number" => get("val_num"),
483 "bool" => Value::Bool(get("val_bool").truthy()),
484 "null" => Value::Null,
485 "json" => parse_json(&get("val_json")),
486 _ => get("val_text"),
487 }
488 }
489
490 fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
493 let rows = match source {
494 Some(s) => self.all(
495 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
496 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
497 )?,
498 None => self.all(
499 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
500 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
501 )?,
502 };
503 if rows.is_empty() {
504 return self.doc_prop_object(doc_id, key, source);
505 }
506 if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
507 return Ok(Self::decode_prop(&rows[0]));
508 }
509 Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
510 }
511
512 fn doc_prop_object(
515 &self,
516 doc_id: &str,
517 prefix: &str,
518 source: Option<&str>,
519 ) -> oqx::Result<Value> {
520 let like = SqlValue::Text(format!("{prefix}.%"));
521 let rows = match source {
522 Some(s) => self.all(
523 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
524 &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
525 )?,
526 None => self.all(
527 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
528 &[SqlValue::Text(doc_id.to_owned()), like],
529 )?,
530 };
531 if rows.is_empty() {
532 return Ok(Value::Undefined);
533 }
534 let mut out = Object::new();
535 for r in &rows {
536 let key = r.get("key").and_then(Value::as_str).unwrap_or("");
537 let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
538 .split('.')
539 .collect();
540 set_nested(&mut out, &rest, Self::decode_prop(r));
541 }
542 Ok(Value::Object(out))
543 }
544
545 fn doc_prop_bag(&self, doc_id: &str, source: &str) -> oqx::Result<Value> {
548 let keys = self.all(
549 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
550 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(source.to_owned())],
551 )?;
552 let mut out = Object::new();
553 for k in keys {
554 let key = k.get("key").and_then(Value::as_str).unwrap_or("");
555 let top = key.split('.').next().unwrap_or("");
556 if !out.contains_key(top) {
557 let v = self.doc_prop(doc_id, top, Some(source))?;
558 out.insert(top, v);
559 }
560 }
561 Ok(Value::Object(out))
562 }
563
564 fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
569 let ordinal = col(block, "ordinal").clone();
570 if col(block, "parent_block").is_absent() {
571 return Ok(ordinal);
572 }
573 let ap = col_str(block, "ancestor_path");
574 let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
575 return Ok(ordinal);
576 };
577 let r = self.scalar(
578 "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
579 &[
580 SqlValue::Text(col_str(block, "doc_id")),
581 SqlValue::Text(first.to_owned()),
582 ],
583 )?;
584 Ok(if r.is_absent() { ordinal } else { r })
585 }
586
587 fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
591 let rows = self.all(
592 "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",
593 &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
594 )?;
595 let ids: Vec<String> = rows
596 .iter()
597 .map(|r| {
598 r.get("block_id")
599 .and_then(Value::as_str)
600 .unwrap_or("")
601 .to_owned()
602 })
603 .collect();
604 let parent_index: Vec<Option<usize>> = rows
605 .iter()
606 .map(|r| {
607 r.get("parent_block")
608 .and_then(Value::as_str)
609 .and_then(|p| ids.iter().position(|id| id == p))
610 })
611 .collect();
612 let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
613 let mut roots = Vec::new();
614 for (i, p) in parent_index.iter().enumerate() {
615 match p {
616 Some(p) => children[*p].push(i),
617 None => roots.push(i),
618 }
619 }
620 fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
621 order.push(i);
622 for &c in &children[i] {
623 walk(c, children, order);
624 }
625 }
626 let mut order = Vec::with_capacity(rows.len());
627 for r in roots {
628 walk(r, &children, &mut order);
629 }
630 let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
631 Ok(order
632 .into_iter()
633 .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
634 .collect())
635 }
636
637 fn jattr(row: &Value, k: &str) -> Value {
638 match parse_json(col(row, "attrs")) {
639 Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
640 _ => Value::Undefined,
641 }
642 }
643
644 fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
646 let path = || SqlValue::Text(col_str(row, "__path"));
647 let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
648 let doc_path = || SqlValue::Text(col_str(row, "path"));
649 let block_id = || SqlValue::Text(col_str(row, "block_id"));
650 let repo = || SqlValue::Text(self.repo_id.clone());
651 Ok(Some(match (t, key) {
652 (Target::Docs, "nodes") => {
653 let rows = self.all(
656 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
657 &[doc_path(), doc_id()],
658 )?;
659 let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
660 let rank: HashMap<String, usize> = blocks
661 .iter()
662 .enumerate()
663 .map(|(i, b)| (col_str(b, "block_id"), i))
664 .collect();
665 let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
666 .into_iter()
667 .map(|r| {
668 let block = r.get("block_id").and_then(Value::as_str);
669 let (has_block, rk) = match block {
670 None => (0, 0),
671 Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
672 };
673 let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
674 let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
675 ((has_block, rk, span, id), r)
676 })
677 .collect();
678 keyed.sort_by(|a, b| {
679 a.0.0
680 .cmp(&b.0.0)
681 .then(a.0.1.cmp(&b.0.1))
682 .then(a.0.2.total_cmp(&b.0.2))
683 .then(a.0.3.cmp(&b.0.3))
684 });
685 Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
686 }
687 (Target::Docs, "blocks") => {
688 Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
689 }
690 (Target::Docs, "out") => Self::tag_all(
691 self.all(
692 "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.dst_node = d2.doc_id
693 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",
694 &[doc_id(), repo()],
695 )?,
696 Target::Docs,
697 ),
698 (Target::Docs, "in") => Self::tag_all(
699 self.all(
700 "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.src_doc = d2.doc_id
701 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",
702 &[doc_id(), repo()],
703 )?,
704 Target::Docs,
705 ),
706 (Target::Docs, "out_edges") => Self::tag_all(
707 self.all(
708 "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",
709 &[doc_path(), doc_id()],
710 )?,
711 Target::Edges,
712 ),
713 (Target::Docs, "in_edges") => Self::tag_all(
714 self.all(
715 "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
716 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
717 &[doc_id()],
718 )?,
719 Target::Edges,
720 ),
721 (Target::Blocks, "children") => Self::tag_all(
722 self.all(
723 "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",
724 &[path(), block_id()],
725 )?,
726 Target::Blocks,
727 ),
728 (Target::Blocks, "nodes") => Self::tag_all(
729 self.all(
730 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
731 &[path(), block_id()],
732 )?,
733 Target::Nodes,
734 ),
735 (Target::Blocks, "out_edges") => Self::tag_all(
736 self.all(
737 "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",
738 &[path(), block_id()],
739 )?,
740 Target::Edges,
741 ),
742 (Target::Blocks, "section") => {
743 let top = to_sql(&self.top_ordinal(row)?);
744 Self::tag_all(
745 self.all(
746 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
747 AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
748 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
749 &[path(), doc_id(), top.clone(), top],
750 )?,
751 Target::Nodes,
752 )
753 }
754 (Target::Nodes, "blocks") => {
755 let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
756 if f.is_absent() || l.is_absent() {
757 return Ok(Some(Value::Array(Vec::new())));
758 }
759 let (f, l) = (
760 f.as_f64().unwrap_or(f64::NAN),
761 l.as_f64().unwrap_or(f64::NAN),
762 );
763 let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
764 let mut kept: Vec<Value> = Vec::new();
765 for b in rows {
766 let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
767 if t >= f && t <= l {
768 kept.push(b);
769 }
770 }
771 Value::Array(kept)
772 }
773 (Target::Nodes, "subsections") => {
774 let (f, l, lvl) = (
775 Self::jattr(row, "first_ordinal"),
776 Self::jattr(row, "last_ordinal"),
777 Self::jattr(row, "level"),
778 );
779 if f.is_absent() {
780 return Ok(Some(Value::Array(Vec::new())));
781 }
782 Self::tag_all(
783 self.all(
784 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
785 AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
786 AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
787 &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
788 )?,
789 Target::Nodes,
790 )
791 }
792 (Target::Nodes, "children") => {
793 let (f, l, lvl) = (
794 Self::jattr(row, "first_ordinal"),
795 Self::jattr(row, "last_ordinal"),
796 Self::jattr(row, "level"),
797 );
798 if f.is_absent() {
799 return Ok(Some(Value::Array(Vec::new())));
800 }
801 Self::tag_all(
802 self.all(
803 "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
804 AND json_extract(i.attrs,'$.level') > ?3
805 AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
806 AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
807 AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
808 AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
809 AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
810 ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
811 &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
812 )?,
813 Target::Nodes,
814 )
815 }
816 _ => return Ok(None),
817 }))
818 }
819
820 fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
821 let id = match col(row, "doc_id") {
822 Value::Undefined | Value::Null => col(row, "src_doc").clone(),
823 v => v.clone(),
824 };
825 Ok(self
826 .one("SELECT * FROM docs WHERE doc_id = ?1", &[to_sql(&id)])?
827 .map_or(Value::Undefined, |o| Self::tag(o, Target::Docs)))
828 }
829
830 fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
831 let id = col(row, "block_id");
832 if !id.truthy() {
833 return Ok(Value::Undefined);
834 }
835 Ok(self
836 .one(
837 "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
838 &[to_sql(id)],
839 )?
840 .map_or(Value::Undefined, |o| Self::tag(o, Target::Blocks)))
841 }
842
843 fn null_if_absent(v: Value) -> Value {
846 if v.is_absent() { Value::Null } else { v }
847 }
848
849 fn intrinsic(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
850 if name == "$self" {
851 return Ok(row.clone());
852 }
853 let c = |k: &str| col(row, k).clone();
854 Ok(match (t, name) {
855 (Target::Docs, "$id") => c("doc_id"),
856 (Target::Docs, "$path") => c("path"),
857 (Target::Docs, "$content_hash") => Self::null_if_absent(c("file_hash")),
858 (Target::Docs, "$updated_at") => Self::null_if_absent(self.scalar(
859 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
860 &[to_sql(&c("current_rev"))],
861 )?),
862 (Target::Docs, "$body") => {
863 match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
864 Ok(Some(s)) => Value::Str(s),
865 Ok(None) => Value::Null,
866 Err(e) => return Err(OqxError::eval(e.to_string())),
867 }
868 }
869 (Target::Docs, "$title") => {
870 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
871 }
872 (Target::Docs, "$tags") => {
873 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$tags", Some("computed"))?)
874 }
875 (Target::Blocks, "$id") => c("block_id"),
876 (Target::Blocks, "$doc") => c("doc_id"),
877 (Target::Blocks, "$path") => c("__path"),
878 (Target::Blocks, "$ordinal") => c("ordinal"),
879 (Target::Blocks, "$depth") => c("depth"),
880 (Target::Blocks, "$body") => c("text"),
881 (Target::Blocks, "$content_hash") => Self::null_if_absent(c("raw_hash")),
882 (Target::Blocks, "$updated_at") => Self::null_if_absent(self.scalar(
883 "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
884 &[to_sql(&c("block_id"))],
885 )?),
886 (Target::Nodes, "$id" | "$node_id") => c("node_id"),
887 (Target::Nodes, "$doc_id") => c("doc_id"),
888 (Target::Nodes, "$block_id") => c("block_id"),
889 (Target::Nodes, "$path") => c("__path"),
890 (Target::Edges, "$id") => c("edge_id"),
891 (Target::Edges, "$src") => c("src_doc"),
892 (Target::Edges, "$dst") => c("dst_node"),
893 (Target::Edges, "$src_block") => c("src_block"),
894 (Target::Edges, "$via") => c("via_node"),
895 (Target::Edges, "$from_commit") => c("from_commit"),
896 (Target::Edges, "$path") => c("__path"),
897 (Target::Edges, "$dst_path") => Self::null_if_absent(self.scalar(
898 "SELECT path FROM docs WHERE doc_id = ?1",
899 &[to_sql(&c("dst_node"))],
900 )?),
901 (Target::Edges, "$dst_uri") => Self::null_if_absent(self.scalar(
902 "SELECT uri FROM external_nodes WHERE node_id = ?1",
903 &[to_sql(&c("dst_node"))],
904 )?),
905 _ => Value::Undefined,
906 })
907 }
908
909 fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
912 Some(Err(OqxError::eval(msg)))
913 }
914
915 fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
916 (t != want).then(|| {
917 Err(OqxError::eval(format!(
918 "{name}() is only available on the {} target",
919 want.as_str()
920 )))
921 })
922 }
923
924 fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
925 r.map(Value::Bool)
926 }
927
928 fn row_method(
929 &self,
930 name: &str,
931 row: &Value,
932 t: Target,
933 args: &[Value],
934 ) -> Option<oqx::Result<Value>> {
935 let c = |k: &str| col(row, k).clone();
936 match name {
937 "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
938 "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
939 "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
940 Some(Self::sql_result(self.exists(
941 "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
942 &[to_sql(&c("block_id"))],
943 )))
944 }),
945 "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
946 Some(self.scalar(
947 "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
948 &[to_sql(&c("block_id"))],
949 ))
950 }),
951 "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
952 Some(
953 self.scalar(
954 "SELECT type FROM blocks WHERE block_id = ?1",
955 &[to_sql(&c("parent_block"))],
956 )
957 .map(Self::null_if_absent),
958 )
959 }),
960 "has_edge" => {
961 let pred = js_string(args.first().unwrap_or(&Value::Undefined));
962 let (src_col, src_val) = if t == Target::Blocks {
963 ("src_block", c("block_id"))
964 } else {
965 ("src_doc", c("doc_id"))
966 };
967 let r = if args.len() >= 2 {
968 self.exists(
969 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
970 &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
971 )
972 } else {
973 self.exists(
974 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
975 &[to_sql(&src_val), SqlValue::Text(pred)],
976 )
977 };
978 Some(Self::sql_result(r))
979 }
980 "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
981 let target = js_string(args.first().unwrap_or(&Value::Undefined));
982 let ap = col_str(row, "ancestor_path");
983 Some(Ok(Value::Bool(
984 ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
985 )))
986 }),
987 "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
988 let text = js_string(args.first().unwrap_or(&Value::Undefined));
989 let top = match self.top_ordinal(row) {
990 Ok(v) => to_sql(&v),
991 Err(e) => return Some(Err(e)),
992 };
993 Some(Self::sql_result(self.exists(
994 "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
995 WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
996 &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
997 )))
998 }),
999 "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1000 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1001 if target.starts_with("d_") {
1002 return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
1003 }
1004 if target.contains('*') {
1005 let like = glob_to_like(&target, false);
1006 return Some(Self::sql_result(self.exists(
1007 "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
1008 &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
1009 )));
1010 }
1011 Some(Ok(Value::Bool(col_str(row, "__path") == target)))
1012 }),
1013 "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1014 let kind = js_string(args.first().unwrap_or(&Value::Undefined));
1015 let ap: Vec<String> = col_str(row, "ancestor_path")
1016 .split('/')
1017 .filter(|s| !s.is_empty())
1018 .map(str::to_owned)
1019 .collect();
1020 if ap.is_empty() {
1021 return Some(Ok(Value::Bool(false)));
1022 }
1023 let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
1024 let placeholders = placeholders.join(",");
1025 let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
1026 let n = params.len();
1027 params.push(SqlValue::Text(kind));
1028 let r = match args.get(1) {
1029 Some(v) if !v.is_absent() => {
1030 let nm = js_string(v);
1031 params.push(SqlValue::Text(nm.clone()));
1032 params.push(SqlValue::Text(nm));
1033 self.exists(
1034 &format!(
1035 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
1036 n + 1,
1037 n + 2,
1038 n + 3
1039 ),
1040 ¶ms,
1041 )
1042 }
1043 _ => self.exists(
1044 &format!(
1045 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
1046 n + 1
1047 ),
1048 ¶ms,
1049 ),
1050 };
1051 Some(Self::sql_result(r))
1052 }),
1053 "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1054 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
1055 }),
1056 "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1057 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
1058 }),
1059 _ => None,
1060 }
1061 }
1062
1063 fn key_path(row: &Value, path: &str, kind: &str) -> Value {
1064 let key = if kind == "json" {
1065 let mut p = path;
1066 p = p.strip_prefix('#').unwrap_or(p);
1067 p = p.strip_prefix('/').unwrap_or(p);
1068 p.split('/').collect::<Vec<_>>().join(".")
1069 } else {
1070 path.to_owned()
1071 };
1072 let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
1073 if !col_str(row, "type").starts_with(&format!("{kind}:")) {
1074 return Value::Bool(false);
1075 }
1076 let k = Self::jattr(row, "key");
1077 Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
1078 }
1079
1080 fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
1081 if t == Target::Edges {
1082 return Err(OqxError::eval(
1083 "text(...) is not available on the edges target",
1084 ));
1085 }
1086 let m = sanitize_fts_query(terms);
1087 if m.is_empty() {
1088 return Ok(Value::Bool(false));
1089 }
1090 let r = match t {
1091 Target::Docs => self.exists(
1092 "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",
1093 &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1094 ),
1095 Target::Nodes => self.exists(
1096 "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1097 &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1098 ),
1099 _ => self.exists(
1100 "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1101 &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1102 ),
1103 };
1104 Self::sql_result(r)
1105 }
1106
1107 fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1117 let flags = match args.get(1) {
1118 None | Some(Value::Undefined) | Some(Value::Null) => None,
1119 Some(Value::Str(s)) => Some(s.clone()),
1120 Some(_) => return None,
1121 };
1122 let Some(subject) = string_form(recv) else {
1123 return Some(Ok(Value::Bool(false)));
1124 };
1125 let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1126 let key = (pattern, flags);
1127 let mut memo = self.regexes.borrow_mut();
1128 if !memo.contains_key(&key) {
1129 let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1130 match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1131 Ok(re) => {
1132 memo.insert(key.clone(), re);
1133 }
1134 Err(_) => return None,
1135 }
1136 }
1137 Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1138 }
1139
1140 fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1141 if matches!(t, Target::Nodes | Target::Edges) {
1142 return Err(OqxError::eval(
1143 "semantic(...) is available on the docs and blocks targets",
1144 ));
1145 }
1146 let Some(resolved) = self.semantic.get(phrase) else {
1147 return Err(OqxError::eval(format!(
1148 "semantic({}) needs an embedding provider; none is configured for this query",
1149 serde_json::Value::String(phrase.to_owned())
1150 )));
1151 };
1152 let vec: oqx::Result<Option<Vec<u8>>> = match t {
1153 Target::Docs => self
1154 .conn
1155 .query_row(
1156 "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1157 rusqlite::params![col_str(row, "doc_id"), resolved.model],
1158 |r| r.get(0),
1159 )
1160 .optional()
1161 .map_err(sql_err),
1162 _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1165 .map_err(|e| OqxError::eval(e.to_string())),
1166 };
1167 match vec? {
1168 Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1169 None => Ok(Value::Null),
1170 }
1171 }
1172}
1173
1174pub(crate) fn fetch_rows(
1177 conn: &Connection,
1178 sql: &str,
1179 params: &[SqlValue],
1180) -> rusqlite::Result<Vec<Object>> {
1181 let mut stmt = conn.prepare_cached(sql)?;
1182 let names: Vec<String> = stmt
1183 .column_names()
1184 .iter()
1185 .map(|s| (*s).to_owned())
1186 .collect();
1187 let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1188 let mut o = Object::with_capacity(names.len() + 1);
1191 for (i, name) in names.iter().enumerate() {
1192 o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1193 }
1194 Ok(o)
1195 })?;
1196 rows.collect()
1197}
1198
1199pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1202 row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1203 Value::Object(row)
1204}
1205
1206pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1208 rows.into_iter().map(|r| tag_row(r, t)).collect()
1209}
1210
1211fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1214 let Some((first, rest)) = path.split_first() else {
1215 return;
1216 };
1217 if rest.is_empty() {
1218 out.insert(*first, leaf);
1219 return;
1220 }
1221 let mut child = match out.get(first) {
1222 Some(Value::Object(o)) => o.clone(),
1223 _ => Object::new(),
1224 };
1225 set_nested(&mut child, rest, leaf);
1226 out.insert(*first, Value::Object(child));
1227}
1228
1229#[must_use]
1232pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1233 let mut out = String::with_capacity(glob.len() + 4);
1234 for ch in glob.chars() {
1235 match ch {
1236 '%' | '_' => {
1237 out.push('\\');
1238 out.push(ch);
1239 }
1240 '\\' if escape_backslash => out.push_str("\\\\"),
1241 '*' => out.push('%'),
1242 c => out.push(c),
1243 }
1244 }
1245 out
1246}
1247
1248impl DataContext for StoreContext<'_> {
1249 fn root(&self, name: &str) -> Value {
1250 if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1251 let mut slot = rr.rows.borrow_mut();
1252 let rows = if rr.once { slot.take() } else { slot.clone() };
1253 return Value::Array(rows.unwrap_or_default());
1254 }
1255 if name == "$repo" {
1256 return self.repo_root();
1257 }
1258 Target::parse(name).map_or(Value::Undefined, scan_marker)
1261 }
1262
1263 fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1264 if row.is_absent() {
1265 return Ok(Value::Undefined);
1266 }
1267 if key == "$repo" {
1270 return Ok(self.repo_root());
1271 }
1272 if is_repo_root(row) {
1273 if key == "$id" {
1274 return Ok(Value::Str(self.repo_id.clone()));
1275 }
1276 return Ok(Target::parse(key).map_or(Value::Undefined, scan_marker));
1277 }
1278 let Some(t) = target_of(row) else {
1279 return Ok(oqx::DefaultContext::read(row, key));
1281 };
1282 if key.starts_with('$') {
1283 return self.intrinsic(row, t, key);
1284 }
1285 match (t, key) {
1287 (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1288 return Ok(row.clone());
1289 }
1290 (_, "doc") => return self.owning_doc(row),
1291 (Target::Nodes, "block") => return self.owning_block(row),
1292 _ => {}
1293 }
1294 if let Some(v) = self.relation(row, t, key)? {
1295 return Ok(v);
1296 }
1297 let c = |k: &str| col(row, k).clone();
1298 Ok(match t {
1299 Target::Docs => {
1300 if key == "format" {
1301 return Ok(c("format"));
1302 }
1303 let doc_id = col_str(row, "doc_id");
1304 if key == "frontmatter" || key == "inline" {
1305 return self.doc_prop_bag(&doc_id, key);
1306 }
1307 if RESERVED_DOC_BASENAMES.contains(&key) {
1308 return Err(OqxError::eval(format!(
1312 "bare '{key}' reads a frontmatter key; did you mean the intrinsic ${key}? (use frontmatter.{key} to force the property)"
1313 )));
1314 }
1315 return self.doc_prop(&doc_id, key, None);
1316 }
1317 Target::Blocks => match key {
1318 "type" => c("type"),
1319 "text" => c("text"),
1320 "attrs" => parse_json(&c("attrs")),
1321 _ => Self::jattr(row, key),
1322 },
1323 Target::Nodes => match key {
1324 "kind" => c("kind"),
1325 "name" => c("name"),
1326 "value" => c("value"),
1327 "attrs" => parse_json(&c("attrs")),
1328 _ => Self::jattr(row, key),
1329 },
1330 Target::Edges => match key {
1331 "predicate" | "provenance" | "dst_kind" | "anchor" | "src_field" => c(key),
1332 _ => Value::Undefined,
1333 },
1334 })
1335 }
1336
1337 fn to_rows(&self, value: &Value) -> Vec<Value> {
1338 match value {
1339 Value::Undefined | Value::Null => Vec::new(),
1340 Value::Array(a) => a.clone(),
1341 other => match scan_of(other) {
1342 Some(t) => self.expand_scan(t),
1343 None => vec![other.clone()],
1344 },
1345 }
1346 }
1347
1348 fn materialize(&self, value: Value) -> Value {
1353 if value.as_object().is_some_and(|o| o.len() == 1)
1354 && let Some(t) = scan_of(&value)
1355 {
1356 return Value::Array(self.expand_scan(t));
1357 }
1358 value
1359 }
1360
1361 fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
1362 crate::store_index::index_for(self, scan_of(collection)?, path)
1363 }
1364
1365 fn identity(&self, row: &Value) -> Value {
1366 match target_of(row) {
1367 Some(Target::Docs) => col(row, "doc_id").clone(),
1368 Some(Target::Blocks) => col(row, "block_id").clone(),
1369 Some(Target::Nodes) => col(row, "node_id").clone(),
1370 Some(Target::Edges) => col(row, "edge_id").clone(),
1371 None => row.clone(),
1372 }
1373 }
1374
1375 fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
1376 if name == "range" {
1379 let x = args.first().unwrap_or(&Value::Undefined);
1380 return Some(Ok(match x {
1381 Value::Range(_) => x.clone(),
1382 Value::Str(s) => match omgbase_properties::detect_range(s) {
1383 Some(r) => {
1384 let b = |b: &Bound| match b {
1385 Bound::Open => Value::Undefined,
1386 Bound::Num(n) => Value::Number(*n),
1387 Bound::Iso(s) => Value::Str(s.clone()),
1388 };
1389 Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
1390 }
1391 None => Value::Null,
1392 },
1393 _ => Value::Null,
1394 }));
1395 }
1396 builtin_function(name, args)
1397 }
1398
1399 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1400 if let Some(t) = target_of(recv) {
1403 if let Some(r) = self.row_method(name, recv, t, args) {
1404 return Some(r);
1405 }
1406 } else if matches!(
1407 name,
1408 "text"
1409 | "semantic"
1410 | "under"
1411 | "under_heading"
1412 | "within"
1413 | "under_kind"
1414 | "yaml_path"
1415 | "json_pointer"
1416 | "has_edge"
1417 | "has_anchor"
1418 | "child_count"
1419 | "parent_type"
1420 ) {
1421 return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/edges row"));
1422 }
1423 if name == "matches" {
1424 if let Some(r) = self.matches_memoized(recv, args) {
1425 return Some(r);
1426 }
1427 }
1428 builtin_method_with(RegexDialect::Oqx, name, recv, args)
1429 }
1430}
1431
1432#[cfg(test)]
1433mod tests {
1434 use super::*;
1435
1436 #[test]
1437 fn glob_to_like_escapes() {
1438 assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
1439 assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
1440 assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
1441 }
1442
1443 #[test]
1444 fn rows_surfacing_as_values_render_id_and_path() {
1445 let mut node = Object::new();
1448 node.insert("node_id", Value::Str("n_1".into()));
1449 node.insert("attrs", Value::Str("{\"checked\":true}".into()));
1450 node.insert("__path", Value::Str("a.md".into()));
1451 let mut doc = Object::new();
1452 doc.insert("doc_id", Value::Str("d_0".into()));
1453 doc.insert("path", Value::Str("a.md".into()));
1454 doc.insert("blob", Value::Str("ff".into()));
1455 let mut record = Object::new();
1456 record.insert(TAG_KEY, Value::Str("junk".into()));
1457 record.insert(
1458 "tasks",
1459 Value::Array(vec![
1460 tag_row(node, Target::Nodes),
1461 tag_row(doc, Target::Docs),
1462 ]),
1463 );
1464 let out = render_row_values(Value::Object(record));
1465 let o = out.as_object().unwrap();
1466 assert!(o.get(TAG_KEY).is_none());
1467 let tasks = o.get("tasks").unwrap().as_array().unwrap();
1468 let keys = |v: &Value| -> Vec<String> {
1469 v.as_object()
1470 .unwrap()
1471 .iter()
1472 .map(|(k, _)| k.to_owned())
1473 .collect()
1474 };
1475 assert_eq!(keys(&tasks[0]), ["id", "path"]);
1476 assert_eq!(
1477 tasks[0].as_object().unwrap().get("id"),
1478 Some(&Value::Str("n_1".into()))
1479 );
1480 assert_eq!(
1481 tasks[0].as_object().unwrap().get("path"),
1482 Some(&Value::Str("a.md".into()))
1483 );
1484 assert_eq!(keys(&tasks[1]), ["id", "path"]);
1485 assert_eq!(
1486 tasks[1].as_object().unwrap().get("id"),
1487 Some(&Value::Str("d_0".into()))
1488 );
1489 let mut edge = Object::new();
1491 edge.insert("edge_id", Value::Number(7.0));
1492 let e = render_row_values(tag_row(edge, Target::Edges));
1493 assert_eq!(
1494 e.as_object().unwrap().get("id"),
1495 Some(&Value::Str("7".into()))
1496 );
1497 assert_eq!(
1498 e.as_object().unwrap().get("path"),
1499 Some(&Value::Str(String::new()))
1500 );
1501 }
1502
1503 #[test]
1504 fn nested_property_objects_rebuild() {
1505 let mut o = Object::new();
1506 set_nested(&mut o, &["a", "b"], Value::Number(1.0));
1507 set_nested(&mut o, &["a", "c"], Value::Number(2.0));
1508 set_nested(&mut o, &["d"], Value::Str("x".into()));
1509 let a = o.get("a").unwrap().as_object().unwrap();
1510 assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
1511 assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
1512 assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
1513 set_nested(&mut o, &["d", "e"], Value::Bool(true));
1515 assert!(o.get("d").unwrap().as_object().is_some());
1516 }
1517
1518 #[test]
1519 fn json_and_sql_bridges() {
1520 assert_eq!(
1521 parse_json(&Value::Str("{\"a\":1}".into()))
1522 .as_object()
1523 .unwrap()
1524 .get("a"),
1525 Some(&Value::Number(1.0))
1526 );
1527 assert_eq!(
1528 parse_json(&Value::Str("nope".into())),
1529 Value::Str("nope".into())
1530 );
1531 assert_eq!(parse_json(&Value::Null), Value::Undefined);
1532 assert_eq!(arg_or_empty(&[], 0), "");
1533 assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
1534 }
1535}