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::planner;
52
53pub const TAG_KEY: &str = "__oqx_target";
55const REPO_TAG: &str = "$repo";
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
220fn is_repo_root(row: &Value) -> bool {
221 row.as_object()
222 .and_then(|o| o.get(TAG_KEY))
223 .and_then(Value::as_str)
224 == Some(REPO_TAG)
225}
226
227fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
228 row.as_object()
229 .and_then(|o| o.get(key))
230 .unwrap_or(&Value::Undefined)
231}
232
233fn col_str(row: &Value, key: &str) -> String {
234 match col(row, key) {
235 Value::Undefined | Value::Null => String::new(),
236 v => js_string(v),
237 }
238}
239
240#[must_use]
242pub fn strip_tags(v: Value) -> Value {
243 match v {
244 Value::Object(o) => Value::Object(
245 o.into_iter()
246 .filter(|(k, _)| k != TAG_KEY)
247 .map(|(k, x)| (k, strip_tags(x)))
248 .collect(),
249 ),
250 Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
251 other => other,
252 }
253}
254
255#[must_use]
262pub fn render_row_values(v: Value) -> Value {
263 match v {
264 Value::Object(o) => {
265 let row = Value::Object(o);
266 if let Some(t) = target_of(&row) {
267 let (id_col, path_col) = match t {
268 Target::Docs => ("doc_id", "path"),
269 Target::Blocks => ("block_id", "__path"),
270 Target::Nodes => ("node_id", "__path"),
271 Target::Edges => ("edge_id", "__path"),
272 };
273 let mut out = Object::with_capacity(2);
274 out.insert("id", Value::Str(col_str(&row, id_col)));
275 out.insert("path", Value::Str(col_str(&row, path_col)));
276 return Value::Object(out);
277 }
278 let Value::Object(o) = row else {
279 unreachable!()
280 };
281 Value::Object(
282 o.into_iter()
283 .filter(|(k, _)| k != TAG_KEY)
284 .map(|(k, x)| (k, render_row_values(x)))
285 .collect(),
286 )
287 }
288 Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
289 other => other,
290 }
291}
292
293impl<'a> StoreContext<'a> {
294 #[must_use]
297 pub fn new(
298 conn: &'a Connection,
299 repo_id: &str,
300 semantic: HashMap<String, SemanticVec>,
301 ) -> Self {
302 Self {
303 conn,
304 repo_id: repo_id.to_owned(),
305 semantic,
306 root_failure: RefCell::new(None),
307 rows_root: None,
308 regexes: RefCell::new(HashMap::new()),
309 scans: RefCell::new(HashMap::new()),
310 scans_run: Cell::new(0),
311 }
312 }
313
314 #[must_use]
316 pub fn repo_id(&self) -> &str {
317 &self.repo_id
318 }
319
320 #[must_use]
324 pub fn scans_run(&self) -> usize {
325 self.scans_run.get()
326 }
327
328 pub(crate) fn scan_rows(&self, t: Target) -> oqx::Result<Rc<Vec<Value>>> {
330 if let Some(rows) = self.scans.borrow().get(&t) {
331 return Ok(Rc::clone(rows));
332 }
333 self.scans_run.set(self.scans_run.get() + 1);
334 let rows = match self.root_scan(t)? {
335 Value::Array(rows) => Rc::new(rows),
336 _ => Rc::new(Vec::new()),
337 };
338 self.scans.borrow_mut().insert(t, Rc::clone(&rows));
339 Ok(rows)
340 }
341
342 fn refs(&self, x: &Value) -> oqx::Result<Value> {
350 let items: Vec<&Value> = match x {
351 Value::Undefined | Value::Null => Vec::new(),
352 Value::Array(a) => a.iter().collect(),
353 other => vec![other],
354 };
355 let mut out = Vec::new();
356 for item in items {
357 if let Value::Str(s) = item
358 && let Some(row) = self.ref_doc(s)?
359 {
360 out.push(row);
361 }
362 }
363 Ok(Value::Array(out))
364 }
365
366 fn ref_doc(&self, r: &str) -> oqx::Result<Option<Value>> {
372 let t = Target::Docs;
373 let lookup = |column: &str, value: &str| -> oqx::Result<Option<Value>> {
374 let sql = format!(
375 "SELECT {} FROM {} WHERE {} AND d.{column} = ? ORDER BY {}",
376 planner::columns(t),
377 planner::from_clause(t),
378 planner::guards(t),
379 planner::order_clause(t)
380 );
381 let params = [
382 SqlValue::Text(self.repo_id.clone()),
383 SqlValue::Text(value.to_owned()),
384 ];
385 Ok(self.one(&sql, ¶ms)?.map(|row| tag_row(row, t)))
386 };
387 if let Some(row) = lookup("path", r.strip_prefix('/').unwrap_or(r))? {
388 return Ok(Some(row));
389 }
390 if r.starts_with("d_") {
391 return lookup("doc_id", r);
392 }
393 Ok(None)
394 }
395
396 pub(crate) fn probe_rows(
398 &self,
399 t: Target,
400 sql: &str,
401 params: &[SqlValue],
402 ) -> oqx::Result<Vec<Value>> {
403 Ok(tag_rows(self.all(sql, params)?, t))
404 }
405
406 fn expand_scan(&self, t: Target) -> Vec<Value> {
409 match self.scan_rows(t) {
410 Ok(rows) => rows.as_ref().clone(),
411 Err(e) => {
412 let mut slot = self.root_failure.borrow_mut();
413 if slot.is_none() {
414 *slot = Some(e);
415 }
416 Vec::new()
417 }
418 }
419 }
420
421 #[must_use]
426 pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
427 self.rows_root = Some(RowsRoot {
428 rows: RefCell::new(Some(rows)),
429 once: false,
430 });
431 self
432 }
433
434 #[must_use]
442 pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
443 self.rows_root = Some(RowsRoot {
444 rows: RefCell::new(Some(rows)),
445 once: true,
446 });
447 self
448 }
449
450 pub fn take_root_failure(&self) -> Option<OqxError> {
455 self.root_failure.borrow_mut().take()
456 }
457
458 fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
461 fetch_rows(self.conn, sql, params).map_err(sql_err)
462 }
463
464 fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
465 Ok(self.all(sql, params)?.into_iter().next())
466 }
467
468 fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
469 Ok(self
470 .one(sql, params)?
471 .and_then(|o| o.values().next().cloned())
472 .unwrap_or(Value::Undefined))
473 }
474
475 fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
476 let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
477 stmt.exists(params_from_iter(params.iter()))
478 .map_err(sql_err)
479 }
480
481 fn tag_all(rows: Vec<Object>, t: Target) -> Value {
482 Value::Array(tag_rows(rows, t))
483 }
484
485 fn tag(row: Object, t: Target) -> Value {
486 tag_row(row, t)
487 }
488
489 fn repo_root(&self) -> Value {
490 let mut o = Object::with_capacity(1);
491 o.insert(TAG_KEY, Value::Str(REPO_TAG.to_owned()));
492 Value::Object(o)
493 }
494
495 fn root_scan(&self, t: Target) -> oqx::Result<Value> {
508 let repo = [SqlValue::Text(self.repo_id.clone())];
509 let sql = match t {
510 Target::Docs => {
511 "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
512 }
513 Target::Blocks => {
514 "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
515 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
516 ORDER BY d.path, b.block_id"
517 }
518 Target::Nodes => {
519 "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
520 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
521 }
522 Target::Edges => {
523 "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
524 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
525 ORDER BY d.path, e.edge_id"
526 }
527 };
528 Ok(Self::tag_all(self.all(sql, &repo)?, t))
529 }
530
531 fn decode_prop(r: &Object) -> Value {
536 let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
537 match get("type").as_str().unwrap_or("") {
538 "number" => get("val_num"),
539 "bool" => Value::Bool(get("val_bool").truthy()),
540 "null" => Value::Null,
541 "json" => parse_json(&get("val_json")),
542 _ => get("val_text"),
543 }
544 }
545
546 fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
549 let rows = match source {
550 Some(s) => self.all(
551 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
552 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
553 )?,
554 None => self.all(
555 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
556 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
557 )?,
558 };
559 if rows.is_empty() {
560 return self.doc_prop_object(doc_id, key, source);
561 }
562 if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
563 return Ok(Self::decode_prop(&rows[0]));
564 }
565 Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
566 }
567
568 fn doc_prop_object(
571 &self,
572 doc_id: &str,
573 prefix: &str,
574 source: Option<&str>,
575 ) -> oqx::Result<Value> {
576 let like = SqlValue::Text(format!("{prefix}.%"));
577 let rows = match source {
578 Some(s) => self.all(
579 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
580 &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
581 )?,
582 None => self.all(
583 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
584 &[SqlValue::Text(doc_id.to_owned()), like],
585 )?,
586 };
587 if rows.is_empty() {
588 return Ok(Value::Undefined);
589 }
590 let mut out = Object::new();
591 for r in &rows {
592 let key = r.get("key").and_then(Value::as_str).unwrap_or("");
593 let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
594 .split('.')
595 .collect();
596 set_nested(&mut out, &rest, Self::decode_prop(r));
597 }
598 Ok(Value::Object(out))
599 }
600
601 fn doc_prop_bag(&self, doc_id: &str, source: &str) -> oqx::Result<Value> {
604 let keys = self.all(
605 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
606 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(source.to_owned())],
607 )?;
608 let mut out = Object::new();
609 for k in keys {
610 let key = k.get("key").and_then(Value::as_str).unwrap_or("");
611 let top = key.split('.').next().unwrap_or("");
612 if !out.contains_key(top) {
613 let v = self.doc_prop(doc_id, top, Some(source))?;
614 out.insert(top, v);
615 }
616 }
617 Ok(Value::Object(out))
618 }
619
620 fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
625 let ordinal = col(block, "ordinal").clone();
626 if col(block, "parent_block").is_absent() {
627 return Ok(ordinal);
628 }
629 let ap = col_str(block, "ancestor_path");
630 let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
631 return Ok(ordinal);
632 };
633 let r = self.scalar(
634 "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
635 &[
636 SqlValue::Text(col_str(block, "doc_id")),
637 SqlValue::Text(first.to_owned()),
638 ],
639 )?;
640 Ok(if r.is_absent() { ordinal } else { r })
641 }
642
643 fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
647 let rows = self.all(
648 "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",
649 &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
650 )?;
651 let ids: Vec<String> = rows
652 .iter()
653 .map(|r| {
654 r.get("block_id")
655 .and_then(Value::as_str)
656 .unwrap_or("")
657 .to_owned()
658 })
659 .collect();
660 let parent_index: Vec<Option<usize>> = rows
661 .iter()
662 .map(|r| {
663 r.get("parent_block")
664 .and_then(Value::as_str)
665 .and_then(|p| ids.iter().position(|id| id == p))
666 })
667 .collect();
668 let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
669 let mut roots = Vec::new();
670 for (i, p) in parent_index.iter().enumerate() {
671 match p {
672 Some(p) => children[*p].push(i),
673 None => roots.push(i),
674 }
675 }
676 fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
677 order.push(i);
678 for &c in &children[i] {
679 walk(c, children, order);
680 }
681 }
682 let mut order = Vec::with_capacity(rows.len());
683 for r in roots {
684 walk(r, &children, &mut order);
685 }
686 let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
687 Ok(order
688 .into_iter()
689 .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
690 .collect())
691 }
692
693 fn jattr(row: &Value, k: &str) -> Value {
694 match parse_json(col(row, "attrs")) {
695 Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
696 _ => Value::Undefined,
697 }
698 }
699
700 fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
702 let path = || SqlValue::Text(col_str(row, "__path"));
703 let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
704 let doc_path = || SqlValue::Text(col_str(row, "path"));
705 let block_id = || SqlValue::Text(col_str(row, "block_id"));
706 let repo = || SqlValue::Text(self.repo_id.clone());
707 Ok(Some(match (t, key) {
708 (Target::Docs, "nodes") => {
709 let rows = self.all(
712 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
713 &[doc_path(), doc_id()],
714 )?;
715 let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
716 let rank: HashMap<String, usize> = blocks
717 .iter()
718 .enumerate()
719 .map(|(i, b)| (col_str(b, "block_id"), i))
720 .collect();
721 let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
722 .into_iter()
723 .map(|r| {
724 let block = r.get("block_id").and_then(Value::as_str);
725 let (has_block, rk) = match block {
726 None => (0, 0),
727 Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
728 };
729 let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
730 let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
731 ((has_block, rk, span, id), r)
732 })
733 .collect();
734 keyed.sort_by(|a, b| {
735 a.0.0
736 .cmp(&b.0.0)
737 .then(a.0.1.cmp(&b.0.1))
738 .then(a.0.2.total_cmp(&b.0.2))
739 .then(a.0.3.cmp(&b.0.3))
740 });
741 Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
742 }
743 (Target::Docs, "blocks") => {
744 Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
745 }
746 (Target::Docs, "out") => Self::tag_all(
747 self.all(
748 "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.dst_node = d2.doc_id
749 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",
750 &[doc_id(), repo()],
751 )?,
752 Target::Docs,
753 ),
754 (Target::Docs, "in") => Self::tag_all(
755 self.all(
756 "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.src_doc = d2.doc_id
757 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",
758 &[doc_id(), repo()],
759 )?,
760 Target::Docs,
761 ),
762 (Target::Docs, "out_edges") => Self::tag_all(
763 self.all(
764 "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",
765 &[doc_path(), doc_id()],
766 )?,
767 Target::Edges,
768 ),
769 (Target::Docs, "in_edges") => Self::tag_all(
770 self.all(
771 "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
772 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
773 &[doc_id()],
774 )?,
775 Target::Edges,
776 ),
777 (Target::Blocks, "children") => Self::tag_all(
778 self.all(
779 "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",
780 &[path(), block_id()],
781 )?,
782 Target::Blocks,
783 ),
784 (Target::Blocks, "nodes") => Self::tag_all(
785 self.all(
786 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
787 &[path(), block_id()],
788 )?,
789 Target::Nodes,
790 ),
791 (Target::Blocks, "out_edges") => Self::tag_all(
792 self.all(
793 "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",
794 &[path(), block_id()],
795 )?,
796 Target::Edges,
797 ),
798 (Target::Blocks, "section") => {
799 let top = to_sql(&self.top_ordinal(row)?);
800 Self::tag_all(
801 self.all(
802 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
803 AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
804 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
805 &[path(), doc_id(), top.clone(), top],
806 )?,
807 Target::Nodes,
808 )
809 }
810 (Target::Nodes, "blocks") => {
811 let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
812 if f.is_absent() || l.is_absent() {
813 return Ok(Some(Value::Array(Vec::new())));
814 }
815 let (f, l) = (
816 f.as_f64().unwrap_or(f64::NAN),
817 l.as_f64().unwrap_or(f64::NAN),
818 );
819 let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
820 let mut kept: Vec<Value> = Vec::new();
821 for b in rows {
822 let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
823 if t >= f && t <= l {
824 kept.push(b);
825 }
826 }
827 Value::Array(kept)
828 }
829 (Target::Nodes, "subsections") => {
830 let (f, l, lvl) = (
831 Self::jattr(row, "first_ordinal"),
832 Self::jattr(row, "last_ordinal"),
833 Self::jattr(row, "level"),
834 );
835 if f.is_absent() {
836 return Ok(Some(Value::Array(Vec::new())));
837 }
838 Self::tag_all(
839 self.all(
840 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
841 AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
842 AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
843 &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
844 )?,
845 Target::Nodes,
846 )
847 }
848 (Target::Nodes, "children") => {
849 let (f, l, lvl) = (
850 Self::jattr(row, "first_ordinal"),
851 Self::jattr(row, "last_ordinal"),
852 Self::jattr(row, "level"),
853 );
854 if f.is_absent() {
855 return Ok(Some(Value::Array(Vec::new())));
856 }
857 Self::tag_all(
858 self.all(
859 "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
860 AND json_extract(i.attrs,'$.level') > ?3
861 AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
862 AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
863 AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
864 AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
865 AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
866 ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
867 &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
868 )?,
869 Target::Nodes,
870 )
871 }
872 _ => return Ok(None),
873 }))
874 }
875
876 fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
877 let id = match col(row, "doc_id") {
878 Value::Undefined | Value::Null => col(row, "src_doc").clone(),
879 v => v.clone(),
880 };
881 Ok(self
882 .one("SELECT * FROM docs WHERE doc_id = ?1", &[to_sql(&id)])?
883 .map_or(Value::Undefined, |o| Self::tag(o, Target::Docs)))
884 }
885
886 fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
887 let id = col(row, "block_id");
888 if !id.truthy() {
889 return Ok(Value::Undefined);
890 }
891 Ok(self
892 .one(
893 "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
894 &[to_sql(id)],
895 )?
896 .map_or(Value::Undefined, |o| Self::tag(o, Target::Blocks)))
897 }
898
899 fn null_if_absent(v: Value) -> Value {
902 if v.is_absent() { Value::Null } else { v }
903 }
904
905 fn intrinsic(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
906 if name == "$self" {
907 return Ok(row.clone());
908 }
909 let c = |k: &str| col(row, k).clone();
910 Ok(match (t, name) {
911 (Target::Docs, "$id") => c("doc_id"),
912 (Target::Docs, "$path") => c("path"),
913 (Target::Docs, "$content_hash") => Self::null_if_absent(c("file_hash")),
914 (Target::Docs, "$updated_at") => Self::null_if_absent(self.scalar(
915 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
916 &[to_sql(&c("current_rev"))],
917 )?),
918 (Target::Docs, "$body") => {
919 match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
920 Ok(Some(s)) => Value::Str(s),
921 Ok(None) => Value::Null,
922 Err(e) => return Err(OqxError::eval(e.to_string())),
923 }
924 }
925 (Target::Docs, "$title") => {
926 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
927 }
928 (Target::Docs, "$tags") => {
929 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$tags", Some("computed"))?)
930 }
931 (Target::Blocks, "$id") => c("block_id"),
932 (Target::Blocks, "$doc") => c("doc_id"),
933 (Target::Blocks, "$path") => c("__path"),
934 (Target::Blocks, "$ordinal") => c("ordinal"),
935 (Target::Blocks, "$depth") => c("depth"),
936 (Target::Blocks, "$body") => c("text"),
937 (Target::Blocks, "$content_hash") => Self::null_if_absent(c("raw_hash")),
938 (Target::Blocks, "$updated_at") => Self::null_if_absent(self.scalar(
939 "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
940 &[to_sql(&c("block_id"))],
941 )?),
942 (Target::Nodes, "$id" | "$node_id") => c("node_id"),
943 (Target::Nodes, "$doc_id") => c("doc_id"),
944 (Target::Nodes, "$block_id") => c("block_id"),
945 (Target::Nodes, "$path") => c("__path"),
946 (Target::Edges, "$id") => c("edge_id"),
947 (Target::Edges, "$src") => c("src_doc"),
948 (Target::Edges, "$dst") => c("dst_node"),
949 (Target::Edges, "$src_block") => c("src_block"),
950 (Target::Edges, "$via") => c("via_node"),
951 (Target::Edges, "$from_commit") => c("from_commit"),
952 (Target::Edges, "$path") => c("__path"),
953 (Target::Edges, "$dst_path") => Self::null_if_absent(self.scalar(
954 "SELECT path FROM docs WHERE doc_id = ?1",
955 &[to_sql(&c("dst_node"))],
956 )?),
957 (Target::Edges, "$dst_uri") => Self::null_if_absent(self.scalar(
958 "SELECT uri FROM external_nodes WHERE node_id = ?1",
959 &[to_sql(&c("dst_node"))],
960 )?),
961 _ => Value::Undefined,
962 })
963 }
964
965 fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
968 Some(Err(OqxError::eval(msg)))
969 }
970
971 fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
972 (t != want).then(|| {
973 Err(OqxError::eval(format!(
974 "{name}() is only available on the {} target",
975 want.as_str()
976 )))
977 })
978 }
979
980 fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
981 r.map(Value::Bool)
982 }
983
984 fn row_method(
985 &self,
986 name: &str,
987 row: &Value,
988 t: Target,
989 args: &[Value],
990 ) -> Option<oqx::Result<Value>> {
991 let c = |k: &str| col(row, k).clone();
992 match name {
993 "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
994 "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
995 "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
996 Some(Self::sql_result(self.exists(
997 "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
998 &[to_sql(&c("block_id"))],
999 )))
1000 }),
1001 "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1002 Some(self.scalar(
1003 "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
1004 &[to_sql(&c("block_id"))],
1005 ))
1006 }),
1007 "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1008 Some(
1009 self.scalar(
1010 "SELECT type FROM blocks WHERE block_id = ?1",
1011 &[to_sql(&c("parent_block"))],
1012 )
1013 .map(Self::null_if_absent),
1014 )
1015 }),
1016 "has_edge" => {
1017 let pred = js_string(args.first().unwrap_or(&Value::Undefined));
1018 let (src_col, src_val) = if t == Target::Blocks {
1019 ("src_block", c("block_id"))
1020 } else {
1021 ("src_doc", c("doc_id"))
1022 };
1023 let r = if args.len() >= 2 {
1024 self.exists(
1025 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
1026 &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
1027 )
1028 } else {
1029 self.exists(
1030 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
1031 &[to_sql(&src_val), SqlValue::Text(pred)],
1032 )
1033 };
1034 Some(Self::sql_result(r))
1035 }
1036 "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1037 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1038 let ap = col_str(row, "ancestor_path");
1039 Some(Ok(Value::Bool(
1040 ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
1041 )))
1042 }),
1043 "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1044 let text = js_string(args.first().unwrap_or(&Value::Undefined));
1045 let top = match self.top_ordinal(row) {
1046 Ok(v) => to_sql(&v),
1047 Err(e) => return Some(Err(e)),
1048 };
1049 Some(Self::sql_result(self.exists(
1050 "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
1051 WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
1052 &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
1053 )))
1054 }),
1055 "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1056 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1057 if target.starts_with("d_") {
1058 return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
1059 }
1060 if target.contains('*') {
1061 let like = glob_to_like(&target, false);
1062 return Some(Self::sql_result(self.exists(
1063 "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
1064 &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
1065 )));
1066 }
1067 Some(Ok(Value::Bool(col_str(row, "__path") == target)))
1068 }),
1069 "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1070 let kind = js_string(args.first().unwrap_or(&Value::Undefined));
1071 let ap: Vec<String> = col_str(row, "ancestor_path")
1072 .split('/')
1073 .filter(|s| !s.is_empty())
1074 .map(str::to_owned)
1075 .collect();
1076 if ap.is_empty() {
1077 return Some(Ok(Value::Bool(false)));
1078 }
1079 let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
1080 let placeholders = placeholders.join(",");
1081 let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
1082 let n = params.len();
1083 params.push(SqlValue::Text(kind));
1084 let r = match args.get(1) {
1085 Some(v) if !v.is_absent() => {
1086 let nm = js_string(v);
1087 params.push(SqlValue::Text(nm.clone()));
1088 params.push(SqlValue::Text(nm));
1089 self.exists(
1090 &format!(
1091 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
1092 n + 1,
1093 n + 2,
1094 n + 3
1095 ),
1096 ¶ms,
1097 )
1098 }
1099 _ => self.exists(
1100 &format!(
1101 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
1102 n + 1
1103 ),
1104 ¶ms,
1105 ),
1106 };
1107 Some(Self::sql_result(r))
1108 }),
1109 "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1110 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
1111 }),
1112 "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1113 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
1114 }),
1115 _ => None,
1116 }
1117 }
1118
1119 fn key_path(row: &Value, path: &str, kind: &str) -> Value {
1120 let key = if kind == "json" {
1121 let mut p = path;
1122 p = p.strip_prefix('#').unwrap_or(p);
1123 p = p.strip_prefix('/').unwrap_or(p);
1124 p.split('/').collect::<Vec<_>>().join(".")
1125 } else {
1126 path.to_owned()
1127 };
1128 let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
1129 if !col_str(row, "type").starts_with(&format!("{kind}:")) {
1130 return Value::Bool(false);
1131 }
1132 let k = Self::jattr(row, "key");
1133 Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
1134 }
1135
1136 fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
1137 if t == Target::Edges {
1138 return Err(OqxError::eval(
1139 "text(...) is not available on the edges target",
1140 ));
1141 }
1142 let m = sanitize_fts_query(terms);
1143 if m.is_empty() {
1144 return Ok(Value::Bool(false));
1145 }
1146 let r = match t {
1147 Target::Docs => self.exists(
1148 "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",
1149 &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1150 ),
1151 Target::Nodes => self.exists(
1152 "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1153 &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1154 ),
1155 _ => self.exists(
1156 "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1157 &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1158 ),
1159 };
1160 Self::sql_result(r)
1161 }
1162
1163 fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1173 let flags = match args.get(1) {
1174 None | Some(Value::Undefined) | Some(Value::Null) => None,
1175 Some(Value::Str(s)) => Some(s.clone()),
1176 Some(_) => return None,
1177 };
1178 let Some(subject) = string_form(recv) else {
1179 return Some(Ok(Value::Bool(false)));
1180 };
1181 let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1182 let key = (pattern, flags);
1183 let mut memo = self.regexes.borrow_mut();
1184 if !memo.contains_key(&key) {
1185 let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1186 match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1187 Ok(re) => {
1188 memo.insert(key.clone(), re);
1189 }
1190 Err(_) => return None,
1191 }
1192 }
1193 Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1194 }
1195
1196 fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1197 if matches!(t, Target::Nodes | Target::Edges) {
1198 return Err(OqxError::eval(
1199 "semantic(...) is available on the docs and blocks targets",
1200 ));
1201 }
1202 let Some(resolved) = self.semantic.get(phrase) else {
1203 return Err(OqxError::eval(format!(
1204 "semantic({}) needs an embedding provider; none is configured for this query",
1205 serde_json::Value::String(phrase.to_owned())
1206 )));
1207 };
1208 let vec: oqx::Result<Option<Vec<u8>>> = match t {
1209 Target::Docs => self
1210 .conn
1211 .query_row(
1212 "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1213 rusqlite::params![col_str(row, "doc_id"), resolved.model],
1214 |r| r.get(0),
1215 )
1216 .optional()
1217 .map_err(sql_err),
1218 _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1221 .map_err(|e| OqxError::eval(e.to_string())),
1222 };
1223 match vec? {
1224 Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1225 None => Ok(Value::Null),
1226 }
1227 }
1228}
1229
1230pub(crate) fn fetch_rows(
1233 conn: &Connection,
1234 sql: &str,
1235 params: &[SqlValue],
1236) -> rusqlite::Result<Vec<Object>> {
1237 let mut stmt = conn.prepare_cached(sql)?;
1238 let names: Vec<String> = stmt
1239 .column_names()
1240 .iter()
1241 .map(|s| (*s).to_owned())
1242 .collect();
1243 let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1244 let mut o = Object::with_capacity(names.len() + 1);
1247 for (i, name) in names.iter().enumerate() {
1248 o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1249 }
1250 Ok(o)
1251 })?;
1252 rows.collect()
1253}
1254
1255pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1258 row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1259 Value::Object(row)
1260}
1261
1262pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1264 rows.into_iter().map(|r| tag_row(r, t)).collect()
1265}
1266
1267fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1270 let Some((first, rest)) = path.split_first() else {
1271 return;
1272 };
1273 if rest.is_empty() {
1274 out.insert(*first, leaf);
1275 return;
1276 }
1277 let mut child = match out.get(first) {
1278 Some(Value::Object(o)) => o.clone(),
1279 _ => Object::new(),
1280 };
1281 set_nested(&mut child, rest, leaf);
1282 out.insert(*first, Value::Object(child));
1283}
1284
1285#[must_use]
1288pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1289 let mut out = String::with_capacity(glob.len() + 4);
1290 for ch in glob.chars() {
1291 match ch {
1292 '%' | '_' => {
1293 out.push('\\');
1294 out.push(ch);
1295 }
1296 '\\' if escape_backslash => out.push_str("\\\\"),
1297 '*' => out.push('%'),
1298 c => out.push(c),
1299 }
1300 }
1301 out
1302}
1303
1304impl DataContext for StoreContext<'_> {
1305 fn root(&self, name: &str) -> Value {
1306 if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1307 let mut slot = rr.rows.borrow_mut();
1308 let rows = if rr.once { slot.take() } else { slot.clone() };
1309 return Value::Array(rows.unwrap_or_default());
1310 }
1311 if name == "$repo" {
1312 return self.repo_root();
1313 }
1314 Target::parse(name).map_or(Value::Undefined, scan_marker)
1317 }
1318
1319 fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1320 if row.is_absent() {
1321 return Ok(Value::Undefined);
1322 }
1323 if key == "$repo" {
1326 return Ok(self.repo_root());
1327 }
1328 if is_repo_root(row) {
1329 if key == "$id" {
1330 return Ok(Value::Str(self.repo_id.clone()));
1331 }
1332 return Ok(Target::parse(key).map_or(Value::Undefined, scan_marker));
1333 }
1334 let Some(t) = target_of(row) else {
1335 return Ok(oqx::DefaultContext::read(row, key));
1337 };
1338 if key.starts_with('$') {
1339 return self.intrinsic(row, t, key);
1340 }
1341 match (t, key) {
1343 (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1344 return Ok(row.clone());
1345 }
1346 (_, "doc") => return self.owning_doc(row),
1347 (Target::Nodes, "block") => return self.owning_block(row),
1348 _ => {}
1349 }
1350 if let Some(v) = self.relation(row, t, key)? {
1351 return Ok(v);
1352 }
1353 let c = |k: &str| col(row, k).clone();
1354 Ok(match t {
1355 Target::Docs => {
1356 if key == "format" {
1357 return Ok(c("format"));
1358 }
1359 let doc_id = col_str(row, "doc_id");
1360 if key == "frontmatter" || key == "inline" {
1361 return self.doc_prop_bag(&doc_id, key);
1362 }
1363 if RESERVED_DOC_BASENAMES.contains(&key) {
1364 return Err(OqxError::eval(format!(
1368 "bare '{key}' reads a frontmatter key; did you mean the intrinsic ${key}? (use frontmatter.{key} to force the property)"
1369 )));
1370 }
1371 return self.doc_prop(&doc_id, key, None);
1372 }
1373 Target::Blocks => match key {
1374 "type" => c("type"),
1375 "text" => c("text"),
1376 "attrs" => parse_json(&c("attrs")),
1377 _ => Self::jattr(row, key),
1378 },
1379 Target::Nodes => match key {
1380 "kind" => c("kind"),
1381 "name" => c("name"),
1382 "value" => c("value"),
1383 "attrs" => parse_json(&c("attrs")),
1384 _ => Self::jattr(row, key),
1385 },
1386 Target::Edges => match key {
1387 "predicate" | "provenance" | "dst_kind" | "anchor" | "src_field" => c(key),
1388 _ => Value::Undefined,
1389 },
1390 })
1391 }
1392
1393 fn to_rows(&self, value: &Value) -> Vec<Value> {
1394 match value {
1395 Value::Undefined | Value::Null => Vec::new(),
1396 Value::Array(a) => a.clone(),
1397 other => match scan_of(other) {
1398 Some(t) => self.expand_scan(t),
1399 None => vec![other.clone()],
1400 },
1401 }
1402 }
1403
1404 fn materialize(&self, value: Value) -> Value {
1409 if value.as_object().is_some_and(|o| o.len() == 1)
1410 && let Some(t) = scan_of(&value)
1411 {
1412 return Value::Array(self.expand_scan(t));
1413 }
1414 value
1415 }
1416
1417 fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
1418 crate::store_index::index_for(self, scan_of(collection)?, path)
1419 }
1420
1421 fn identity(&self, row: &Value) -> Value {
1422 match target_of(row) {
1423 Some(Target::Docs) => col(row, "doc_id").clone(),
1424 Some(Target::Blocks) => col(row, "block_id").clone(),
1425 Some(Target::Nodes) => col(row, "node_id").clone(),
1426 Some(Target::Edges) => col(row, "edge_id").clone(),
1427 None => row.clone(),
1428 }
1429 }
1430
1431 fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
1432 if name == "refs" {
1435 return Some(self.refs(args.first().unwrap_or(&Value::Undefined)));
1436 }
1437 if name == "range" {
1438 let x = args.first().unwrap_or(&Value::Undefined);
1439 return Some(Ok(match x {
1440 Value::Range(_) => x.clone(),
1441 Value::Str(s) => match omgbase_properties::detect_range(s) {
1442 Some(r) => {
1443 let b = |b: &Bound| match b {
1444 Bound::Open => Value::Undefined,
1445 Bound::Num(n) => Value::Number(*n),
1446 Bound::Iso(s) => Value::Str(s.clone()),
1447 };
1448 Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
1449 }
1450 None => Value::Null,
1451 },
1452 _ => Value::Null,
1453 }));
1454 }
1455 builtin_function(name, args)
1456 }
1457
1458 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1459 if let Some(t) = target_of(recv) {
1462 if let Some(r) = self.row_method(name, recv, t, args) {
1463 return Some(r);
1464 }
1465 } else if matches!(
1466 name,
1467 "text"
1468 | "semantic"
1469 | "under"
1470 | "under_heading"
1471 | "within"
1472 | "under_kind"
1473 | "yaml_path"
1474 | "json_pointer"
1475 | "has_edge"
1476 | "has_anchor"
1477 | "child_count"
1478 | "parent_type"
1479 ) {
1480 return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/edges row"));
1481 }
1482 if name == "matches" {
1483 if let Some(r) = self.matches_memoized(recv, args) {
1484 return Some(r);
1485 }
1486 }
1487 builtin_method_with(RegexDialect::Oqx, name, recv, args)
1488 }
1489}
1490
1491#[cfg(test)]
1492mod tests {
1493 use super::*;
1494
1495 #[test]
1496 fn glob_to_like_escapes() {
1497 assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
1498 assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
1499 assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
1500 }
1501
1502 #[test]
1503 fn rows_surfacing_as_values_render_id_and_path() {
1504 let mut node = Object::new();
1507 node.insert("node_id", Value::Str("n_1".into()));
1508 node.insert("attrs", Value::Str("{\"checked\":true}".into()));
1509 node.insert("__path", Value::Str("a.md".into()));
1510 let mut doc = Object::new();
1511 doc.insert("doc_id", Value::Str("d_0".into()));
1512 doc.insert("path", Value::Str("a.md".into()));
1513 doc.insert("blob", Value::Str("ff".into()));
1514 let mut record = Object::new();
1515 record.insert(TAG_KEY, Value::Str("junk".into()));
1516 record.insert(
1517 "tasks",
1518 Value::Array(vec![
1519 tag_row(node, Target::Nodes),
1520 tag_row(doc, Target::Docs),
1521 ]),
1522 );
1523 let out = render_row_values(Value::Object(record));
1524 let o = out.as_object().unwrap();
1525 assert!(o.get(TAG_KEY).is_none());
1526 let tasks = o.get("tasks").unwrap().as_array().unwrap();
1527 let keys = |v: &Value| -> Vec<String> {
1528 v.as_object()
1529 .unwrap()
1530 .iter()
1531 .map(|(k, _)| k.to_owned())
1532 .collect()
1533 };
1534 assert_eq!(keys(&tasks[0]), ["id", "path"]);
1535 assert_eq!(
1536 tasks[0].as_object().unwrap().get("id"),
1537 Some(&Value::Str("n_1".into()))
1538 );
1539 assert_eq!(
1540 tasks[0].as_object().unwrap().get("path"),
1541 Some(&Value::Str("a.md".into()))
1542 );
1543 assert_eq!(keys(&tasks[1]), ["id", "path"]);
1544 assert_eq!(
1545 tasks[1].as_object().unwrap().get("id"),
1546 Some(&Value::Str("d_0".into()))
1547 );
1548 let mut edge = Object::new();
1550 edge.insert("edge_id", Value::Number(7.0));
1551 let e = render_row_values(tag_row(edge, Target::Edges));
1552 assert_eq!(
1553 e.as_object().unwrap().get("id"),
1554 Some(&Value::Str("7".into()))
1555 );
1556 assert_eq!(
1557 e.as_object().unwrap().get("path"),
1558 Some(&Value::Str(String::new()))
1559 );
1560 }
1561
1562 #[test]
1563 fn nested_property_objects_rebuild() {
1564 let mut o = Object::new();
1565 set_nested(&mut o, &["a", "b"], Value::Number(1.0));
1566 set_nested(&mut o, &["a", "c"], Value::Number(2.0));
1567 set_nested(&mut o, &["d"], Value::Str("x".into()));
1568 let a = o.get("a").unwrap().as_object().unwrap();
1569 assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
1570 assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
1571 assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
1572 set_nested(&mut o, &["d", "e"], Value::Bool(true));
1574 assert!(o.get("d").unwrap().as_object().is_some());
1575 }
1576
1577 #[test]
1578 fn json_and_sql_bridges() {
1579 assert_eq!(
1580 parse_json(&Value::Str("{\"a\":1}".into()))
1581 .as_object()
1582 .unwrap()
1583 .get("a"),
1584 Some(&Value::Number(1.0))
1585 );
1586 assert_eq!(
1587 parse_json(&Value::Str("nope".into())),
1588 Value::Str("nope".into())
1589 );
1590 assert_eq!(parse_json(&Value::Null), Value::Undefined);
1591 assert_eq!(arg_or_empty(&[], 0), "");
1592 assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
1593 }
1594}