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
51use crate::paths::{reference_path, storage_path};
52use crate::planner;
53
54pub const TAG_KEY: &str = "__oqx_target";
56pub const SCAN_KEY: &str = "__oqx_scan";
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
69pub enum Target {
70 Docs,
71 Blocks,
72 Nodes,
73 Edges,
74}
75
76impl Target {
77 #[must_use]
79 pub fn as_str(self) -> &'static str {
80 match self {
81 Target::Docs => "docs",
82 Target::Blocks => "blocks",
83 Target::Nodes => "nodes",
84 Target::Edges => "edges",
85 }
86 }
87
88 #[must_use]
90 pub fn parse(s: &str) -> Option<Self> {
91 Some(match s {
92 "docs" => Target::Docs,
93 "blocks" => Target::Blocks,
94 "nodes" => Target::Nodes,
95 "edges" => Target::Edges,
96 _ => return None,
97 })
98 }
99}
100
101const RESERVED_DOC_BASENAMES: [&str; 5] = ["id", "path", "updated_at", "content_hash", "body"];
104
105#[derive(Clone, Debug, PartialEq)]
108pub struct SemanticVec {
109 pub model: String,
110 pub vec: Vec<u8>,
111}
112
113pub struct StoreContext<'a> {
115 conn: &'a Connection,
116 repo_id: String,
117 semantic: HashMap<String, SemanticVec>,
118 root_failure: RefCell<Option<OqxError>>,
121 rows_root: Option<RowsRoot>,
123 regexes: RefCell<HashMap<(String, Option<String>), CompiledRegex>>,
126 scans: RefCell<HashMap<Target, Rc<Vec<Value>>>>,
130 scans_run: Cell<usize>,
132}
133
134struct RowsRoot {
139 rows: RefCell<Option<Vec<Value>>>,
140 once: bool,
141}
142
143fn sql_value(v: ValueRef<'_>) -> Value {
144 match v {
145 ValueRef::Null => Value::Null,
146 ValueRef::Integer(i) => Value::Number(i as f64),
147 ValueRef::Real(f) => Value::Number(f),
148 ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
149 ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
150 }
151}
152
153pub(crate) fn to_sql(v: &Value) -> SqlValue {
158 match v {
159 Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
160 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
161 Value::Number(n) => SqlValue::Real(*n),
162 Value::Str(s) => SqlValue::Text(s.clone()),
163 Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
164 }
165}
166
167fn js_string(v: &Value) -> String {
169 v.to_string()
170}
171
172fn arg_or_empty(args: &[Value], i: usize) -> String {
174 match args.get(i) {
175 None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
176 Some(v) => js_string(v),
177 }
178}
179
180fn parse_json(v: &Value) -> Value {
182 match v {
183 Value::Str(s) => {
184 serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
185 }
186 Value::Null | Value::Undefined => Value::Undefined,
187 other => other.clone(),
188 }
189}
190
191fn sql_err(e: rusqlite::Error) -> OqxError {
194 OqxError::eval(format!("sqlite: {e}"))
195}
196
197pub fn target_of(row: &Value) -> Option<Target> {
199 row.as_object()
200 .and_then(|o| o.get(TAG_KEY))
201 .and_then(Value::as_str)
202 .and_then(Target::parse)
203}
204
205#[must_use]
207pub fn scan_of(v: &Value) -> Option<Target> {
208 v.as_object()
209 .and_then(|o| o.get(SCAN_KEY))
210 .and_then(Value::as_str)
211 .and_then(Target::parse)
212}
213
214fn scan_marker(t: Target) -> Value {
215 let mut o = Object::with_capacity(1);
216 o.insert(SCAN_KEY, Value::Str(t.as_str().to_owned()));
217 Value::Object(o)
218}
219
220const ALL_TARGETS: [Target; 4] = [Target::Docs, Target::Blocks, Target::Nodes, Target::Edges];
222
223fn is_root_object(row: &Value) -> bool {
228 row.as_object().is_some_and(|o| {
229 o.len() == ALL_TARGETS.len()
230 && ALL_TARGETS
231 .iter()
232 .all(|t| o.get(t.as_str()).is_some_and(|v| scan_of(v) == Some(*t)))
233 })
234}
235
236fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
237 row.as_object()
238 .and_then(|o| o.get(key))
239 .unwrap_or(&Value::Undefined)
240}
241
242fn col_str(row: &Value, key: &str) -> String {
243 match col(row, key) {
244 Value::Undefined | Value::Null => String::new(),
245 v => js_string(v),
246 }
247}
248
249#[must_use]
251pub fn strip_tags(v: Value) -> Value {
252 match v {
253 Value::Object(o) => Value::Object(
254 o.into_iter()
255 .filter(|(k, _)| k != TAG_KEY)
256 .map(|(k, x)| (k, strip_tags(x)))
257 .collect(),
258 ),
259 Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
260 other => other,
261 }
262}
263
264#[must_use]
272pub fn render_row_values(v: Value) -> Value {
273 match v {
274 Value::Object(o) => {
275 let row = Value::Object(o);
276 if let Some(t) = target_of(&row) {
277 let (id_col, path_col) = match t {
278 Target::Docs => ("doc_id", "path"),
279 Target::Blocks => ("block_id", "__path"),
280 Target::Nodes => ("node_id", "__path"),
281 Target::Edges => ("edge_id", "__path"),
282 };
283 let mut out = Object::with_capacity(2);
284 out.insert("id", Value::Str(col_str(&row, id_col)));
285 let path = match col(&row, path_col) {
286 Value::Undefined | Value::Null => String::new(),
287 v => reference_path(&js_string(v)),
288 };
289 out.insert("path", Value::Str(path));
290 return Value::Object(out);
291 }
292 let Value::Object(o) = row else {
293 unreachable!()
294 };
295 Value::Object(
296 o.into_iter()
297 .filter(|(k, _)| k != TAG_KEY)
298 .map(|(k, x)| (k, render_row_values(x)))
299 .collect(),
300 )
301 }
302 Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
303 other => other,
304 }
305}
306
307impl<'a> StoreContext<'a> {
308 #[must_use]
311 pub fn new(
312 conn: &'a Connection,
313 repo_id: &str,
314 semantic: HashMap<String, SemanticVec>,
315 ) -> Self {
316 Self {
317 conn,
318 repo_id: repo_id.to_owned(),
319 semantic,
320 root_failure: RefCell::new(None),
321 rows_root: None,
322 regexes: RefCell::new(HashMap::new()),
323 scans: RefCell::new(HashMap::new()),
324 scans_run: Cell::new(0),
325 }
326 }
327
328 #[must_use]
330 pub fn repo_id(&self) -> &str {
331 &self.repo_id
332 }
333
334 #[must_use]
338 pub fn scans_run(&self) -> usize {
339 self.scans_run.get()
340 }
341
342 pub(crate) fn scan_rows(&self, t: Target) -> oqx::Result<Rc<Vec<Value>>> {
344 if let Some(rows) = self.scans.borrow().get(&t) {
345 return Ok(Rc::clone(rows));
346 }
347 self.scans_run.set(self.scans_run.get() + 1);
348 let rows = match self.root_scan(t)? {
349 Value::Array(rows) => Rc::new(rows),
350 _ => Rc::new(Vec::new()),
351 };
352 self.scans.borrow_mut().insert(t, Rc::clone(&rows));
353 Ok(rows)
354 }
355
356 fn refs(&self, x: &Value) -> oqx::Result<Value> {
364 let items: Vec<&Value> = match x {
365 Value::Undefined | Value::Null => Vec::new(),
366 Value::Array(a) => a.iter().collect(),
367 other => vec![other],
368 };
369 let mut out = Vec::new();
370 for item in items {
371 if let Value::Str(s) = item
372 && let Some(row) = self.ref_doc(s)?
373 {
374 out.push(row);
375 }
376 }
377 Ok(Value::Array(out))
378 }
379
380 fn ref_doc(&self, r: &str) -> oqx::Result<Option<Value>> {
386 let t = Target::Docs;
387 let lookup = |column: &str, value: &str| -> oqx::Result<Option<Value>> {
388 let sql = format!(
389 "SELECT {} FROM {} WHERE {} AND d.{column} = ? ORDER BY {}",
390 planner::columns(t),
391 planner::from_clause(t),
392 planner::guards(t),
393 planner::order_clause(t)
394 );
395 let params = [
396 SqlValue::Text(self.repo_id.clone()),
397 SqlValue::Text(value.to_owned()),
398 ];
399 Ok(self.one(&sql, ¶ms)?.map(|row| tag_row(row, t)))
400 };
401 if let Some(row) = lookup("path", storage_path(r))? {
402 return Ok(Some(row));
403 }
404 if r.starts_with("d_") {
405 return lookup("doc_id", r);
406 }
407 Ok(None)
408 }
409
410 pub(crate) fn probe_rows(
412 &self,
413 t: Target,
414 sql: &str,
415 params: &[SqlValue],
416 ) -> oqx::Result<Vec<Value>> {
417 Ok(tag_rows(self.all(sql, params)?, t))
418 }
419
420 fn expand_scan(&self, t: Target) -> Vec<Value> {
423 match self.scan_rows(t) {
424 Ok(rows) => rows.as_ref().clone(),
425 Err(e) => {
426 let mut slot = self.root_failure.borrow_mut();
427 if slot.is_none() {
428 *slot = Some(e);
429 }
430 Vec::new()
431 }
432 }
433 }
434
435 #[must_use]
440 pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
441 self.rows_root = Some(RowsRoot {
442 rows: RefCell::new(Some(rows)),
443 once: false,
444 });
445 self
446 }
447
448 #[must_use]
456 pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
457 self.rows_root = Some(RowsRoot {
458 rows: RefCell::new(Some(rows)),
459 once: true,
460 });
461 self
462 }
463
464 pub fn take_root_failure(&self) -> Option<OqxError> {
469 self.root_failure.borrow_mut().take()
470 }
471
472 fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
475 fetch_rows(self.conn, sql, params).map_err(sql_err)
476 }
477
478 fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
479 Ok(self.all(sql, params)?.into_iter().next())
480 }
481
482 fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
483 Ok(self
484 .one(sql, params)?
485 .and_then(|o| o.values().next().cloned())
486 .unwrap_or(Value::Undefined))
487 }
488
489 fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
490 let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
491 stmt.exists(params_from_iter(params.iter()))
492 .map_err(sql_err)
493 }
494
495 fn tag_all(rows: Vec<Object>, t: Target) -> Value {
496 Value::Array(tag_rows(rows, t))
497 }
498
499 fn tag(row: Object, t: Target) -> Value {
500 tag_row(row, t)
501 }
502
503 fn root_row(&self) -> Value {
510 let mut o = Object::with_capacity(ALL_TARGETS.len());
511 for t in ALL_TARGETS {
512 o.insert(t.as_str(), scan_marker(t));
513 }
514 Value::Object(o)
515 }
516
517 fn root_scan(&self, t: Target) -> oqx::Result<Value> {
530 let repo = [SqlValue::Text(self.repo_id.clone())];
531 let sql = match t {
532 Target::Docs => {
533 "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
534 }
535 Target::Blocks => {
536 "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
537 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
538 ORDER BY d.path, b.block_id"
539 }
540 Target::Nodes => {
541 "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
542 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
543 }
544 Target::Edges => {
545 "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
546 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
547 ORDER BY d.path, e.edge_id"
548 }
549 };
550 Ok(Self::tag_all(self.all(sql, &repo)?, t))
551 }
552
553 fn decode_prop(r: &Object) -> Value {
558 let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
559 match get("type").as_str().unwrap_or("") {
560 "number" => get("val_num"),
561 "bool" => Value::Bool(get("val_bool").truthy()),
562 "null" => Value::Null,
563 "json" => parse_json(&get("val_json")),
564 _ => get("val_text"),
565 }
566 }
567
568 fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
571 let rows = match source {
572 Some(s) => self.all(
573 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
574 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
575 )?,
576 None => self.all(
577 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
578 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
579 )?,
580 };
581 if rows.is_empty() {
582 return self.doc_prop_object(doc_id, key, source);
583 }
584 if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
585 return Ok(Self::decode_prop(&rows[0]));
586 }
587 Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
588 }
589
590 fn doc_prop_object(
593 &self,
594 doc_id: &str,
595 prefix: &str,
596 source: Option<&str>,
597 ) -> oqx::Result<Value> {
598 let like = SqlValue::Text(format!("{prefix}.%"));
599 let rows = match source {
600 Some(s) => self.all(
601 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
602 &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
603 )?,
604 None => self.all(
605 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
606 &[SqlValue::Text(doc_id.to_owned()), like],
607 )?,
608 };
609 if rows.is_empty() {
610 return Ok(Value::Undefined);
611 }
612 let mut out = Object::new();
613 for r in &rows {
614 let key = r.get("key").and_then(Value::as_str).unwrap_or("");
615 let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
616 .split('.')
617 .collect();
618 set_nested(&mut out, &rest, Self::decode_prop(r));
619 }
620 Ok(Value::Object(out))
621 }
622
623 fn doc_prop_bag(&self, doc_id: &str, source: &str) -> oqx::Result<Value> {
626 let keys = self.all(
627 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
628 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(source.to_owned())],
629 )?;
630 let mut out = Object::new();
631 for k in keys {
632 let key = k.get("key").and_then(Value::as_str).unwrap_or("");
633 let top = key.split('.').next().unwrap_or("");
634 if !out.contains_key(top) {
635 let v = self.doc_prop(doc_id, top, Some(source))?;
636 out.insert(top, v);
637 }
638 }
639 Ok(Value::Object(out))
640 }
641
642 fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
647 let ordinal = col(block, "ordinal").clone();
648 if col(block, "parent_block").is_absent() {
649 return Ok(ordinal);
650 }
651 let ap = col_str(block, "ancestor_path");
652 let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
653 return Ok(ordinal);
654 };
655 let r = self.scalar(
656 "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
657 &[
658 SqlValue::Text(col_str(block, "doc_id")),
659 SqlValue::Text(first.to_owned()),
660 ],
661 )?;
662 Ok(if r.is_absent() { ordinal } else { r })
663 }
664
665 fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
669 let rows = self.all(
670 "SELECT b.*, ?1 AS __path FROM blocks b WHERE b.doc_id = ?2 AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id",
671 &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
672 )?;
673 let ids: Vec<String> = rows
674 .iter()
675 .map(|r| {
676 r.get("block_id")
677 .and_then(Value::as_str)
678 .unwrap_or("")
679 .to_owned()
680 })
681 .collect();
682 let parent_index: Vec<Option<usize>> = rows
683 .iter()
684 .map(|r| {
685 r.get("parent_block")
686 .and_then(Value::as_str)
687 .and_then(|p| ids.iter().position(|id| id == p))
688 })
689 .collect();
690 let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
691 let mut roots = Vec::new();
692 for (i, p) in parent_index.iter().enumerate() {
693 match p {
694 Some(p) => children[*p].push(i),
695 None => roots.push(i),
696 }
697 }
698 fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
699 order.push(i);
700 for &c in &children[i] {
701 walk(c, children, order);
702 }
703 }
704 let mut order = Vec::with_capacity(rows.len());
705 for r in roots {
706 walk(r, &children, &mut order);
707 }
708 let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
709 Ok(order
710 .into_iter()
711 .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
712 .collect())
713 }
714
715 fn jattr(row: &Value, k: &str) -> Value {
716 match parse_json(col(row, "attrs")) {
717 Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
718 _ => Value::Undefined,
719 }
720 }
721
722 fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
724 let path = || SqlValue::Text(col_str(row, "__path"));
725 let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
726 let doc_path = || SqlValue::Text(col_str(row, "path"));
727 let block_id = || SqlValue::Text(col_str(row, "block_id"));
728 let repo = || SqlValue::Text(self.repo_id.clone());
729 Ok(Some(match (t, key) {
730 (Target::Docs, "nodes") => {
731 let rows = self.all(
734 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
735 &[doc_path(), doc_id()],
736 )?;
737 let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
738 let rank: HashMap<String, usize> = blocks
739 .iter()
740 .enumerate()
741 .map(|(i, b)| (col_str(b, "block_id"), i))
742 .collect();
743 let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
744 .into_iter()
745 .map(|r| {
746 let block = r.get("block_id").and_then(Value::as_str);
747 let (has_block, rk) = match block {
748 None => (0, 0),
749 Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
750 };
751 let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
752 let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
753 ((has_block, rk, span, id), r)
754 })
755 .collect();
756 keyed.sort_by(|a, b| {
757 a.0.0
758 .cmp(&b.0.0)
759 .then(a.0.1.cmp(&b.0.1))
760 .then(a.0.2.total_cmp(&b.0.2))
761 .then(a.0.3.cmp(&b.0.3))
762 });
763 Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
764 }
765 (Target::Docs, "blocks") => {
766 Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
767 }
768 (Target::Docs, "out") => Self::tag_all(
769 self.all(
770 "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.dst_node = d2.doc_id
771 WHERE e.src_doc = ?1 AND e.to_commit IS NULL AND d2.repo_id = ?2 AND d2.deleted_commit IS NULL ORDER BY d2.path, d2.doc_id",
772 &[doc_id(), repo()],
773 )?,
774 Target::Docs,
775 ),
776 (Target::Docs, "in") => Self::tag_all(
777 self.all(
778 "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.src_doc = d2.doc_id
779 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d2.repo_id = ?2 AND d2.deleted_commit IS NULL ORDER BY d2.path, d2.doc_id",
780 &[doc_id(), repo()],
781 )?,
782 Target::Docs,
783 ),
784 (Target::Docs, "out_edges") => Self::tag_all(
785 self.all(
786 "SELECT e.*, ?1 AS __path FROM edges e WHERE e.src_doc = ?2 AND e.to_commit IS NULL ORDER BY e.predicate, e.edge_id",
787 &[doc_path(), doc_id()],
788 )?,
789 Target::Edges,
790 ),
791 (Target::Docs, "in_edges") => Self::tag_all(
792 self.all(
793 "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
794 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
795 &[doc_id()],
796 )?,
797 Target::Edges,
798 ),
799 (Target::Blocks, "children") => Self::tag_all(
800 self.all(
801 "SELECT b.*, ?1 AS __path FROM blocks b WHERE b.parent_block = ?2 AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id",
802 &[path(), block_id()],
803 )?,
804 Target::Blocks,
805 ),
806 (Target::Blocks, "nodes") => Self::tag_all(
807 self.all(
808 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
809 &[path(), block_id()],
810 )?,
811 Target::Nodes,
812 ),
813 (Target::Blocks, "out_edges") => Self::tag_all(
814 self.all(
815 "SELECT e.*, ?1 AS __path FROM edges e WHERE e.src_block = ?2 AND e.to_commit IS NULL ORDER BY e.predicate, e.edge_id",
816 &[path(), block_id()],
817 )?,
818 Target::Edges,
819 ),
820 (Target::Blocks, "section") => {
821 let top = to_sql(&self.top_ordinal(row)?);
822 Self::tag_all(
823 self.all(
824 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
825 AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
826 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
827 &[path(), doc_id(), top.clone(), top],
828 )?,
829 Target::Nodes,
830 )
831 }
832 (Target::Nodes, "blocks") => {
833 let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
834 if f.is_absent() || l.is_absent() {
835 return Ok(Some(Value::Array(Vec::new())));
836 }
837 let (f, l) = (
838 f.as_f64().unwrap_or(f64::NAN),
839 l.as_f64().unwrap_or(f64::NAN),
840 );
841 let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
842 let mut kept: Vec<Value> = Vec::new();
843 for b in rows {
844 let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
845 if t >= f && t <= l {
846 kept.push(b);
847 }
848 }
849 Value::Array(kept)
850 }
851 (Target::Nodes, "subsections") => {
852 let (f, l, lvl) = (
853 Self::jattr(row, "first_ordinal"),
854 Self::jattr(row, "last_ordinal"),
855 Self::jattr(row, "level"),
856 );
857 if f.is_absent() {
858 return Ok(Some(Value::Array(Vec::new())));
859 }
860 Self::tag_all(
861 self.all(
862 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
863 AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
864 AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
865 &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
866 )?,
867 Target::Nodes,
868 )
869 }
870 (Target::Nodes, "children") => {
871 let (f, l, lvl) = (
872 Self::jattr(row, "first_ordinal"),
873 Self::jattr(row, "last_ordinal"),
874 Self::jattr(row, "level"),
875 );
876 if f.is_absent() {
877 return Ok(Some(Value::Array(Vec::new())));
878 }
879 Self::tag_all(
880 self.all(
881 "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
882 AND json_extract(i.attrs,'$.level') > ?3
883 AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
884 AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
885 AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
886 AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
887 AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
888 ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
889 &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
890 )?,
891 Target::Nodes,
892 )
893 }
894 _ => return Ok(None),
895 }))
896 }
897
898 fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
899 let id = match col(row, "doc_id") {
900 Value::Undefined | Value::Null => col(row, "src_doc").clone(),
901 v => v.clone(),
902 };
903 Ok(self
904 .one("SELECT * FROM docs WHERE doc_id = ?1", &[to_sql(&id)])?
905 .map_or(Value::Undefined, |o| Self::tag(o, Target::Docs)))
906 }
907
908 fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
909 let id = col(row, "block_id");
910 if !id.truthy() {
911 return Ok(Value::Undefined);
912 }
913 Ok(self
914 .one(
915 "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
916 &[to_sql(id)],
917 )?
918 .map_or(Value::Undefined, |o| Self::tag(o, Target::Blocks)))
919 }
920
921 fn null_if_absent(v: Value) -> Value {
924 if v.is_absent() { Value::Null } else { v }
925 }
926
927 fn intrinsic(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
932 if name == "$self" {
933 return Ok(row.clone());
934 }
935 let c = |k: &str| col(row, k).clone();
936 let rooted = |k: &str| Value::Str(reference_path(&col_str(row, k)));
937 Ok(match (t, name) {
938 (Target::Docs, "$id") => c("doc_id"),
939 (Target::Docs, "$path") => rooted("path"),
940 (Target::Docs, "$content_hash") => Self::null_if_absent(c("file_hash")),
941 (Target::Docs, "$updated_at") => Self::null_if_absent(self.scalar(
942 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
943 &[to_sql(&c("current_rev"))],
944 )?),
945 (Target::Docs, "$body") => {
946 match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
947 Ok(Some(s)) => Value::Str(s),
948 Ok(None) => Value::Null,
949 Err(e) => return Err(OqxError::eval(e.to_string())),
950 }
951 }
952 (Target::Docs, "$title") => {
953 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
954 }
955 (Target::Docs, "$tags") => {
956 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$tags", Some("computed"))?)
957 }
958 (Target::Blocks, "$id") => c("block_id"),
959 (Target::Blocks, "$doc") => c("doc_id"),
960 (Target::Blocks, "$path") => rooted("__path"),
961 (Target::Blocks, "$ordinal") => c("ordinal"),
962 (Target::Blocks, "$depth") => c("depth"),
963 (Target::Blocks, "$body") => c("text"),
964 (Target::Blocks, "$content_hash") => Self::null_if_absent(c("raw_hash")),
965 (Target::Blocks, "$updated_at") => Self::null_if_absent(self.scalar(
966 "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
967 &[to_sql(&c("block_id"))],
968 )?),
969 (Target::Nodes, "$id" | "$node_id") => c("node_id"),
970 (Target::Nodes, "$doc_id") => c("doc_id"),
971 (Target::Nodes, "$block_id") => c("block_id"),
972 (Target::Nodes, "$path") => rooted("__path"),
973 (Target::Edges, "$id") => c("edge_id"),
974 (Target::Edges, "$src") => c("src_doc"),
975 (Target::Edges, "$dst") => c("dst_node"),
976 (Target::Edges, "$src_block") => c("src_block"),
977 (Target::Edges, "$via") => c("via_node"),
978 (Target::Edges, "$from_commit") => c("from_commit"),
979 (Target::Edges, "$path") => rooted("__path"),
980 (Target::Edges, "$dst_path") => match self.scalar(
981 "SELECT path FROM docs WHERE doc_id = ?1",
982 &[to_sql(&c("dst_node"))],
983 )? {
984 Value::Undefined | Value::Null => Value::Null,
985 p => Value::Str(reference_path(&js_string(&p))),
986 },
987 (Target::Edges, "$dst_uri") => Self::null_if_absent(self.scalar(
988 "SELECT uri FROM external_nodes WHERE node_id = ?1",
989 &[to_sql(&c("dst_node"))],
990 )?),
991 _ => Value::Undefined,
992 })
993 }
994
995 fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
998 Some(Err(OqxError::eval(msg)))
999 }
1000
1001 fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
1002 (t != want).then(|| {
1003 Err(OqxError::eval(format!(
1004 "{name}() is only available on the {} target",
1005 want.as_str()
1006 )))
1007 })
1008 }
1009
1010 fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
1011 r.map(Value::Bool)
1012 }
1013
1014 fn row_method(
1015 &self,
1016 name: &str,
1017 row: &Value,
1018 t: Target,
1019 args: &[Value],
1020 ) -> Option<oqx::Result<Value>> {
1021 let c = |k: &str| col(row, k).clone();
1022 match name {
1023 "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
1024 "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
1025 "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1026 Some(Self::sql_result(self.exists(
1027 "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
1028 &[to_sql(&c("block_id"))],
1029 )))
1030 }),
1031 "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1032 Some(self.scalar(
1033 "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
1034 &[to_sql(&c("block_id"))],
1035 ))
1036 }),
1037 "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1038 Some(
1039 self.scalar(
1040 "SELECT type FROM blocks WHERE block_id = ?1",
1041 &[to_sql(&c("parent_block"))],
1042 )
1043 .map(Self::null_if_absent),
1044 )
1045 }),
1046 "has_edge" => {
1047 let pred = js_string(args.first().unwrap_or(&Value::Undefined));
1048 let (src_col, src_val) = if t == Target::Blocks {
1049 ("src_block", c("block_id"))
1050 } else {
1051 ("src_doc", c("doc_id"))
1052 };
1053 let r = if args.len() >= 2 {
1054 self.exists(
1055 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
1056 &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
1057 )
1058 } else {
1059 self.exists(
1060 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
1061 &[to_sql(&src_val), SqlValue::Text(pred)],
1062 )
1063 };
1064 Some(Self::sql_result(r))
1065 }
1066 "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1067 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1068 let ap = col_str(row, "ancestor_path");
1069 Some(Ok(Value::Bool(
1070 ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
1071 )))
1072 }),
1073 "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1074 let text = js_string(args.first().unwrap_or(&Value::Undefined));
1075 let top = match self.top_ordinal(row) {
1076 Ok(v) => to_sql(&v),
1077 Err(e) => return Some(Err(e)),
1078 };
1079 Some(Self::sql_result(self.exists(
1080 "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
1081 WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
1082 &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
1083 )))
1084 }),
1085 "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1088 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1089 if target.starts_with("d_") {
1090 return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
1091 }
1092 let target = storage_path(&target).to_owned();
1093 if target.contains('*') {
1094 let like = glob_to_like(&target, false);
1095 return Some(Self::sql_result(self.exists(
1096 "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
1097 &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
1098 )));
1099 }
1100 Some(Ok(Value::Bool(col_str(row, "__path") == target)))
1101 }),
1102 "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1103 let kind = js_string(args.first().unwrap_or(&Value::Undefined));
1104 let ap: Vec<String> = col_str(row, "ancestor_path")
1105 .split('/')
1106 .filter(|s| !s.is_empty())
1107 .map(str::to_owned)
1108 .collect();
1109 if ap.is_empty() {
1110 return Some(Ok(Value::Bool(false)));
1111 }
1112 let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
1113 let placeholders = placeholders.join(",");
1114 let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
1115 let n = params.len();
1116 params.push(SqlValue::Text(kind));
1117 let r = match args.get(1) {
1118 Some(v) if !v.is_absent() => {
1119 let nm = js_string(v);
1120 params.push(SqlValue::Text(nm.clone()));
1121 params.push(SqlValue::Text(nm));
1122 self.exists(
1123 &format!(
1124 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
1125 n + 1,
1126 n + 2,
1127 n + 3
1128 ),
1129 ¶ms,
1130 )
1131 }
1132 _ => self.exists(
1133 &format!(
1134 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
1135 n + 1
1136 ),
1137 ¶ms,
1138 ),
1139 };
1140 Some(Self::sql_result(r))
1141 }),
1142 "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1143 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
1144 }),
1145 "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1146 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
1147 }),
1148 _ => None,
1149 }
1150 }
1151
1152 fn key_path(row: &Value, path: &str, kind: &str) -> Value {
1153 let key = if kind == "json" {
1154 let mut p = path;
1155 p = p.strip_prefix('#').unwrap_or(p);
1156 p = p.strip_prefix('/').unwrap_or(p);
1157 p.split('/').collect::<Vec<_>>().join(".")
1158 } else {
1159 path.to_owned()
1160 };
1161 let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
1162 if !col_str(row, "type").starts_with(&format!("{kind}:")) {
1163 return Value::Bool(false);
1164 }
1165 let k = Self::jattr(row, "key");
1166 Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
1167 }
1168
1169 fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
1170 if t == Target::Edges {
1171 return Err(OqxError::eval(
1172 "text(...) is not available on the edges target",
1173 ));
1174 }
1175 let m = sanitize_fts_query(terms);
1176 if m.is_empty() {
1177 return Ok(Value::Bool(false));
1178 }
1179 let r = match t {
1180 Target::Docs => self.exists(
1181 "SELECT 1 FROM blocks_fts JOIN blocks b ON b.rowid = blocks_fts.rowid WHERE b.doc_id = ?1 AND blocks_fts MATCH ?2 LIMIT 1",
1182 &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1183 ),
1184 Target::Nodes => self.exists(
1185 "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1186 &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1187 ),
1188 _ => self.exists(
1189 "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1190 &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1191 ),
1192 };
1193 Self::sql_result(r)
1194 }
1195
1196 fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1206 let flags = match args.get(1) {
1207 None | Some(Value::Undefined) | Some(Value::Null) => None,
1208 Some(Value::Str(s)) => Some(s.clone()),
1209 Some(_) => return None,
1210 };
1211 let Some(subject) = string_form(recv) else {
1212 return Some(Ok(Value::Bool(false)));
1213 };
1214 let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1215 let key = (pattern, flags);
1216 let mut memo = self.regexes.borrow_mut();
1217 if !memo.contains_key(&key) {
1218 let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1219 match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1220 Ok(re) => {
1221 memo.insert(key.clone(), re);
1222 }
1223 Err(_) => return None,
1224 }
1225 }
1226 Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1227 }
1228
1229 fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1230 if matches!(t, Target::Nodes | Target::Edges) {
1231 return Err(OqxError::eval(
1232 "semantic(...) is available on the docs and blocks targets",
1233 ));
1234 }
1235 let Some(resolved) = self.semantic.get(phrase) else {
1236 return Err(OqxError::eval(format!(
1237 "semantic({}) needs an embedding provider; none is configured for this query",
1238 serde_json::Value::String(phrase.to_owned())
1239 )));
1240 };
1241 let vec: oqx::Result<Option<Vec<u8>>> = match t {
1242 Target::Docs => self
1243 .conn
1244 .query_row(
1245 "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1246 rusqlite::params![col_str(row, "doc_id"), resolved.model],
1247 |r| r.get(0),
1248 )
1249 .optional()
1250 .map_err(sql_err),
1251 _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1254 .map_err(|e| OqxError::eval(e.to_string())),
1255 };
1256 match vec? {
1257 Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1258 None => Ok(Value::Null),
1259 }
1260 }
1261}
1262
1263pub(crate) fn fetch_rows(
1266 conn: &Connection,
1267 sql: &str,
1268 params: &[SqlValue],
1269) -> rusqlite::Result<Vec<Object>> {
1270 let mut stmt = conn.prepare_cached(sql)?;
1271 let names: Vec<String> = stmt
1272 .column_names()
1273 .iter()
1274 .map(|s| (*s).to_owned())
1275 .collect();
1276 let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1277 let mut o = Object::with_capacity(names.len() + 1);
1280 for (i, name) in names.iter().enumerate() {
1281 o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1282 }
1283 Ok(o)
1284 })?;
1285 rows.collect()
1286}
1287
1288pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1291 row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1292 Value::Object(row)
1293}
1294
1295pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1297 rows.into_iter().map(|r| tag_row(r, t)).collect()
1298}
1299
1300fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1303 let Some((first, rest)) = path.split_first() else {
1304 return;
1305 };
1306 if rest.is_empty() {
1307 out.insert(*first, leaf);
1308 return;
1309 }
1310 let mut child = match out.get(first) {
1311 Some(Value::Object(o)) => o.clone(),
1312 _ => Object::new(),
1313 };
1314 set_nested(&mut child, rest, leaf);
1315 out.insert(*first, Value::Object(child));
1316}
1317
1318#[must_use]
1321pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1322 let mut out = String::with_capacity(glob.len() + 4);
1323 for ch in glob.chars() {
1324 match ch {
1325 '%' | '_' => {
1326 out.push('\\');
1327 out.push(ch);
1328 }
1329 '\\' if escape_backslash => out.push_str("\\\\"),
1330 '*' => out.push('%'),
1331 c => out.push(c),
1332 }
1333 }
1334 out
1335}
1336
1337impl DataContext for StoreContext<'_> {
1338 fn root(&self, name: &str) -> Value {
1339 if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1340 let mut slot = rr.rows.borrow_mut();
1341 let rows = if rr.once { slot.take() } else { slot.clone() };
1342 return Value::Array(rows.unwrap_or_default());
1343 }
1344 if name == "$id" {
1348 return Value::Str(self.repo_id.clone());
1349 }
1350 Target::parse(name).map_or(Value::Undefined, scan_marker)
1353 }
1354
1355 fn root_object(&self) -> Value {
1358 self.root_row()
1359 }
1360
1361 fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1362 if row.is_absent() {
1363 return Ok(Value::Undefined);
1364 }
1365 if is_root_object(row) {
1368 if key == "$id" {
1369 return Ok(Value::Str(self.repo_id.clone()));
1370 }
1371 return Ok(Target::parse(key).map_or(Value::Undefined, scan_marker));
1372 }
1373 let Some(t) = target_of(row) else {
1374 return Ok(oqx::DefaultContext::read(row, key));
1376 };
1377 if key.starts_with('$') {
1378 return self.intrinsic(row, t, key);
1379 }
1380 match (t, key) {
1382 (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1383 return Ok(row.clone());
1384 }
1385 (_, "doc") => return self.owning_doc(row),
1386 (Target::Nodes, "block") => return self.owning_block(row),
1387 _ => {}
1388 }
1389 if let Some(v) = self.relation(row, t, key)? {
1390 return Ok(v);
1391 }
1392 if let Some(named) = Target::parse(key) {
1398 return Err(OqxError::eval(crate::query::bare_target_message(
1399 named, None,
1400 )));
1401 }
1402 let c = |k: &str| col(row, k).clone();
1403 Ok(match t {
1404 Target::Docs => {
1405 if key == "format" {
1406 return Ok(c("format"));
1407 }
1408 let doc_id = col_str(row, "doc_id");
1409 if key == "frontmatter" || key == "inline" {
1410 return self.doc_prop_bag(&doc_id, key);
1411 }
1412 if RESERVED_DOC_BASENAMES.contains(&key) {
1413 return Err(OqxError::eval(format!(
1417 "bare '{key}' reads a frontmatter key; did you mean the intrinsic ${key}? (use frontmatter.{key} to force the property)"
1418 )));
1419 }
1420 return self.doc_prop(&doc_id, key, None);
1421 }
1422 Target::Blocks => match key {
1423 "type" => c("type"),
1424 "text" => c("text"),
1425 "attrs" => parse_json(&c("attrs")),
1426 _ => Self::jattr(row, key),
1427 },
1428 Target::Nodes => match key {
1429 "kind" => c("kind"),
1430 "name" => c("name"),
1431 "value" => c("value"),
1432 "attrs" => parse_json(&c("attrs")),
1433 _ => Self::jattr(row, key),
1434 },
1435 Target::Edges => match key {
1436 "predicate" | "provenance" | "dst_kind" | "anchor" | "src_field" => c(key),
1437 _ => Value::Undefined,
1438 },
1439 })
1440 }
1441
1442 fn to_rows(&self, value: &Value) -> Vec<Value> {
1443 match value {
1444 Value::Undefined | Value::Null => Vec::new(),
1445 Value::Array(a) => a.clone(),
1446 other => match scan_of(other) {
1447 Some(t) => self.expand_scan(t),
1448 None => vec![other.clone()],
1449 },
1450 }
1451 }
1452
1453 fn materialize(&self, value: Value) -> Value {
1458 if value.as_object().is_some_and(|o| o.len() == 1)
1459 && let Some(t) = scan_of(&value)
1460 {
1461 return Value::Array(self.expand_scan(t));
1462 }
1463 value
1464 }
1465
1466 fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
1467 crate::store_index::index_for(self, scan_of(collection)?, path)
1468 }
1469
1470 fn identity(&self, row: &Value) -> Value {
1471 match target_of(row) {
1472 Some(Target::Docs) => col(row, "doc_id").clone(),
1473 Some(Target::Blocks) => col(row, "block_id").clone(),
1474 Some(Target::Nodes) => col(row, "node_id").clone(),
1475 Some(Target::Edges) => col(row, "edge_id").clone(),
1476 None => row.clone(),
1477 }
1478 }
1479
1480 fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
1481 if name == "refs" {
1484 return Some(self.refs(args.first().unwrap_or(&Value::Undefined)));
1485 }
1486 if name == "range" {
1487 let x = args.first().unwrap_or(&Value::Undefined);
1488 return Some(Ok(match x {
1489 Value::Range(_) => x.clone(),
1490 Value::Str(s) => match omgbase_properties::detect_range(s) {
1491 Some(r) => {
1492 let b = |b: &Bound| match b {
1493 Bound::Open => Value::Undefined,
1494 Bound::Num(n) => Value::Number(*n),
1495 Bound::Iso(s) => Value::Str(s.clone()),
1496 };
1497 Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
1498 }
1499 None => Value::Null,
1500 },
1501 _ => Value::Null,
1502 }));
1503 }
1504 builtin_function(name, args)
1505 }
1506
1507 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1508 if let Some(t) = target_of(recv) {
1511 if let Some(r) = self.row_method(name, recv, t, args) {
1512 return Some(r);
1513 }
1514 } else if matches!(
1515 name,
1516 "text"
1517 | "semantic"
1518 | "under"
1519 | "under_heading"
1520 | "within"
1521 | "under_kind"
1522 | "yaml_path"
1523 | "json_pointer"
1524 | "has_edge"
1525 | "has_anchor"
1526 | "child_count"
1527 | "parent_type"
1528 ) {
1529 return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/edges row"));
1530 }
1531 if name == "matches" {
1532 if let Some(r) = self.matches_memoized(recv, args) {
1533 return Some(r);
1534 }
1535 }
1536 builtin_method_with(RegexDialect::Oqx, name, recv, args)
1537 }
1538}
1539
1540#[cfg(test)]
1541mod tests {
1542 use super::*;
1543
1544 #[test]
1545 fn glob_to_like_escapes() {
1546 assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
1547 assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
1548 assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
1549 }
1550
1551 #[test]
1552 fn rows_surfacing_as_values_render_id_and_path() {
1553 let mut node = Object::new();
1556 node.insert("node_id", Value::Str("n_1".into()));
1557 node.insert("attrs", Value::Str("{\"checked\":true}".into()));
1558 node.insert("__path", Value::Str("a.md".into()));
1559 let mut doc = Object::new();
1560 doc.insert("doc_id", Value::Str("d_0".into()));
1561 doc.insert("path", Value::Str("a.md".into()));
1562 doc.insert("blob", Value::Str("ff".into()));
1563 let mut record = Object::new();
1564 record.insert(TAG_KEY, Value::Str("junk".into()));
1565 record.insert(
1566 "tasks",
1567 Value::Array(vec![
1568 tag_row(node, Target::Nodes),
1569 tag_row(doc, Target::Docs),
1570 ]),
1571 );
1572 let out = render_row_values(Value::Object(record));
1573 let o = out.as_object().unwrap();
1574 assert!(o.get(TAG_KEY).is_none());
1575 let tasks = o.get("tasks").unwrap().as_array().unwrap();
1576 let keys = |v: &Value| -> Vec<String> {
1577 v.as_object()
1578 .unwrap()
1579 .iter()
1580 .map(|(k, _)| k.to_owned())
1581 .collect()
1582 };
1583 assert_eq!(keys(&tasks[0]), ["id", "path"]);
1584 assert_eq!(
1585 tasks[0].as_object().unwrap().get("id"),
1586 Some(&Value::Str("n_1".into()))
1587 );
1588 assert_eq!(
1589 tasks[0].as_object().unwrap().get("path"),
1590 Some(&Value::Str("/a.md".into()))
1591 );
1592 assert_eq!(keys(&tasks[1]), ["id", "path"]);
1593 assert_eq!(
1594 tasks[1].as_object().unwrap().get("id"),
1595 Some(&Value::Str("d_0".into()))
1596 );
1597 let mut edge = Object::new();
1599 edge.insert("edge_id", Value::Number(7.0));
1600 let e = render_row_values(tag_row(edge, Target::Edges));
1601 assert_eq!(
1602 e.as_object().unwrap().get("id"),
1603 Some(&Value::Str("7".into()))
1604 );
1605 assert_eq!(
1606 e.as_object().unwrap().get("path"),
1607 Some(&Value::Str(String::new()))
1608 );
1609 }
1610
1611 #[test]
1612 fn nested_property_objects_rebuild() {
1613 let mut o = Object::new();
1614 set_nested(&mut o, &["a", "b"], Value::Number(1.0));
1615 set_nested(&mut o, &["a", "c"], Value::Number(2.0));
1616 set_nested(&mut o, &["d"], Value::Str("x".into()));
1617 let a = o.get("a").unwrap().as_object().unwrap();
1618 assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
1619 assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
1620 assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
1621 set_nested(&mut o, &["d", "e"], Value::Bool(true));
1623 assert!(o.get("d").unwrap().as_object().is_some());
1624 }
1625
1626 #[test]
1627 fn json_and_sql_bridges() {
1628 assert_eq!(
1629 parse_json(&Value::Str("{\"a\":1}".into()))
1630 .as_object()
1631 .unwrap()
1632 .get("a"),
1633 Some(&Value::Number(1.0))
1634 );
1635 assert_eq!(
1636 parse_json(&Value::Str("nope".into())),
1637 Value::Str("nope".into())
1638 );
1639 assert_eq!(parse_json(&Value::Null), Value::Undefined);
1640 assert_eq!(arg_or_empty(&[], 0), "");
1641 assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
1642 }
1643}