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 Links,
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::Links => "links",
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 "links" => Target::Links,
96 _ => return None,
97 })
98 }
99}
100
101const DOC_ATTRIBUTES: [&str; 8] = [
123 "id",
124 "path",
125 "raw",
126 "hash",
127 "updated_at",
128 "title",
129 "tags",
130 "format",
131];
132const BLOCK_ATTRIBUTES: [&str; 11] = [
133 "id",
134 "path",
135 "raw",
136 "hash",
137 "updated_at",
138 "doc_id",
139 "type",
140 "text",
141 "attrs",
142 "depth",
143 "ordinal",
144];
145const NODE_ATTRIBUTES: [&str; 8] = [
146 "id", "path", "doc_id", "block_id", "kind", "name", "value", "attrs",
147];
148const EDGE_ATTRIBUTES: [&str; 13] = [
149 "id",
150 "path",
151 "doc_id",
152 "block_id",
153 "via_id",
154 "dst_id",
155 "dst_path",
156 "uri",
157 "field",
158 "dst_kind",
159 "predicate",
160 "provenance",
161 "anchor",
162];
163#[must_use]
165pub fn attributes(t: Target) -> &'static [&'static str] {
166 match t {
167 Target::Docs => &DOC_ATTRIBUTES,
168 Target::Blocks => &BLOCK_ATTRIBUTES,
169 Target::Nodes => &NODE_ATTRIBUTES,
170 Target::Links => &EDGE_ATTRIBUTES,
171 }
172}
173pub const SOURCE_HANDLES: [&str; 3] = ["data", "frontmatter", "inline"];
176
177pub const LAZY_KEY: &str = "__oqx_lazy";
191
192pub const ABSENT_KEY: &str = "__oqx_absent";
200
201fn absent_single() -> Value {
202 let mut o = Object::with_capacity(1);
203 o.insert(ABSENT_KEY, Value::Bool(true));
204 Value::Object(o)
205}
206
207fn is_absent_single(v: &Value) -> bool {
209 v.as_object()
210 .is_some_and(|o| o.len() == 1 && o.contains_key(ABSENT_KEY))
211}
212
213#[derive(Clone, Debug, PartialEq)]
216pub struct SemanticVec {
217 pub model: String,
218 pub vec: Vec<u8>,
219}
220
221pub struct StoreContext<'a> {
223 conn: &'a Connection,
224 repo_id: String,
225 semantic: HashMap<String, SemanticVec>,
226 root_failure: RefCell<Option<OqxError>>,
229 rows_root: Option<RowsRoot>,
231 regexes: RefCell<HashMap<(String, Option<String>), CompiledRegex>>,
234 scans: RefCell<HashMap<Target, Rc<Vec<Value>>>>,
238 scans_run: Cell<usize>,
240 statements_run: Cell<usize>,
243 owners: RefCell<HashMap<(Target, String), Option<Value>>>,
246}
247
248struct RowsRoot {
253 rows: RefCell<Option<Vec<Value>>>,
254 once: bool,
255}
256
257fn sql_value(v: ValueRef<'_>) -> Value {
258 match v {
259 ValueRef::Null => Value::Null,
260 ValueRef::Integer(i) => Value::Number(i as f64),
261 ValueRef::Real(f) => Value::Number(f),
262 ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
263 ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
264 }
265}
266
267pub(crate) fn to_sql(v: &Value) -> SqlValue {
272 match v {
273 Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
274 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
275 Value::Number(n) => SqlValue::Real(*n),
276 Value::Str(s) => SqlValue::Text(s.clone()),
277 Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
278 }
279}
280
281fn js_string(v: &Value) -> String {
283 v.to_string()
284}
285
286fn arg_or_empty(args: &[Value], i: usize) -> String {
288 match args.get(i) {
289 None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
290 Some(v) => js_string(v),
291 }
292}
293
294fn parse_json(v: &Value) -> Value {
296 match v {
297 Value::Str(s) => {
298 serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
299 }
300 Value::Null | Value::Undefined => Value::Undefined,
301 other => other.clone(),
302 }
303}
304
305fn sql_err(e: rusqlite::Error) -> OqxError {
308 OqxError::eval(format!("sqlite: {e}"))
309}
310
311pub fn target_of(row: &Value) -> Option<Target> {
313 row.as_object()
314 .and_then(|o| o.get(TAG_KEY))
315 .and_then(Value::as_str)
316 .and_then(Target::parse)
317}
318
319fn is_lazy_owner(row: &Value) -> bool {
321 row.as_object().is_some_and(|o| o.contains_key(LAZY_KEY))
322}
323
324#[must_use]
326pub fn scan_of(v: &Value) -> Option<Target> {
327 v.as_object()
328 .and_then(|o| o.get(SCAN_KEY))
329 .and_then(Value::as_str)
330 .and_then(Target::parse)
331}
332
333fn scan_marker(t: Target) -> Value {
334 let mut o = Object::with_capacity(1);
335 o.insert(SCAN_KEY, Value::Str(t.as_str().to_owned()));
336 Value::Object(o)
337}
338
339const ALL_TARGETS: [Target; 4] = [Target::Docs, Target::Blocks, Target::Nodes, Target::Links];
341
342fn is_root_object(row: &Value) -> bool {
347 row.as_object().is_some_and(|o| {
348 o.len() == ALL_TARGETS.len()
349 && ALL_TARGETS
350 .iter()
351 .all(|t| o.get(t.as_str()).is_some_and(|v| scan_of(v) == Some(*t)))
352 })
353}
354
355fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
356 row.as_object()
357 .and_then(|o| o.get(key))
358 .unwrap_or(&Value::Undefined)
359}
360
361fn col_str(row: &Value, key: &str) -> String {
362 match col(row, key) {
363 Value::Undefined | Value::Null => String::new(),
364 v => js_string(v),
365 }
366}
367
368#[must_use]
370pub fn strip_tags(v: Value) -> Value {
371 match v {
372 Value::Object(o) => Value::Object(
373 o.into_iter()
374 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
375 .map(|(k, x)| (k, strip_tags(x)))
376 .collect(),
377 ),
378 Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
379 other => other,
380 }
381}
382
383#[must_use]
392pub fn render_row_values(v: Value) -> Value {
393 match v {
394 Value::Object(o) => {
395 let row = Value::Object(o);
396 if let Some(t) = target_of(&row) {
397 let (id_col, path_col) = match t {
398 Target::Docs => ("doc_id", "path"),
399 Target::Blocks => ("block_id", "__path"),
400 Target::Nodes => ("node_id", "__path"),
401 Target::Links => ("edge_id", "__path"),
402 };
403 let mut out = Object::with_capacity(2);
404 out.insert(crate::query::HIT_ID, Value::Str(col_str(&row, id_col)));
405 let path = match col(&row, path_col) {
406 Value::Undefined | Value::Null => String::new(),
407 v => reference_path(&js_string(v)),
408 };
409 out.insert(crate::query::HIT_PATH, Value::Str(path));
410 return Value::Object(out);
411 }
412 let Value::Object(o) = row else {
413 unreachable!()
414 };
415 Value::Object(
416 o.into_iter()
417 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
418 .map(|(k, x)| (k, render_row_values(x)))
419 .collect(),
420 )
421 }
422 Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
423 other => other,
424 }
425}
426
427impl<'a> StoreContext<'a> {
428 #[must_use]
431 pub fn new(
432 conn: &'a Connection,
433 repo_id: &str,
434 semantic: HashMap<String, SemanticVec>,
435 ) -> Self {
436 Self {
437 conn,
438 repo_id: repo_id.to_owned(),
439 semantic,
440 root_failure: RefCell::new(None),
441 rows_root: None,
442 regexes: RefCell::new(HashMap::new()),
443 scans: RefCell::new(HashMap::new()),
444 scans_run: Cell::new(0),
445 statements_run: Cell::new(0),
446 owners: RefCell::new(HashMap::new()),
447 }
448 }
449
450 #[must_use]
453 pub fn statements_run(&self) -> usize {
454 self.statements_run.get()
455 }
456
457 #[must_use]
459 pub fn repo_id(&self) -> &str {
460 &self.repo_id
461 }
462
463 #[must_use]
467 pub fn scans_run(&self) -> usize {
468 self.scans_run.get()
469 }
470
471 pub(crate) fn scan_rows(&self, t: Target) -> oqx::Result<Rc<Vec<Value>>> {
473 if let Some(rows) = self.scans.borrow().get(&t) {
474 return Ok(Rc::clone(rows));
475 }
476 self.scans_run.set(self.scans_run.get() + 1);
477 let rows = match self.root_scan(t)? {
478 Value::Array(rows) => Rc::new(rows),
479 _ => Rc::new(Vec::new()),
480 };
481 self.scans.borrow_mut().insert(t, Rc::clone(&rows));
482 Ok(rows)
483 }
484
485 fn refs(&self, x: &Value) -> oqx::Result<Value> {
493 let items: Vec<&Value> = match x {
494 Value::Undefined | Value::Null => Vec::new(),
495 Value::Array(a) => a.iter().collect(),
496 other => vec![other],
497 };
498 let mut out = Vec::new();
499 for item in items {
500 if let Value::Str(s) = item
501 && let Some(row) = self.ref_doc(s)?
502 {
503 out.push(row);
504 }
505 }
506 Ok(Value::Array(out))
507 }
508
509 fn ref_doc(&self, r: &str) -> oqx::Result<Option<Value>> {
515 let t = Target::Docs;
516 let lookup = |column: &str, value: &str| -> oqx::Result<Option<Value>> {
517 let sql = format!(
518 "SELECT {} FROM {} WHERE {} AND d.{column} = ? ORDER BY {}",
519 planner::columns(t),
520 planner::from_clause(t),
521 planner::guards(t),
522 planner::order_clause(t)
523 );
524 let params = [
525 SqlValue::Text(self.repo_id.clone()),
526 SqlValue::Text(value.to_owned()),
527 ];
528 Ok(self.one(&sql, ¶ms)?.map(|row| tag_row(row, t)))
529 };
530 if let Some(row) = lookup("path", storage_path(r))? {
531 return Ok(Some(row));
532 }
533 if r.starts_with("d_") {
534 return lookup("doc_id", r);
535 }
536 Ok(None)
537 }
538
539 pub(crate) fn probe_rows(
541 &self,
542 t: Target,
543 sql: &str,
544 params: &[SqlValue],
545 ) -> oqx::Result<Vec<Value>> {
546 Ok(tag_rows(self.all(sql, params)?, t))
547 }
548
549 fn expand_scan(&self, t: Target) -> Vec<Value> {
552 match self.scan_rows(t) {
553 Ok(rows) => rows.as_ref().clone(),
554 Err(e) => {
555 let mut slot = self.root_failure.borrow_mut();
556 if slot.is_none() {
557 *slot = Some(e);
558 }
559 Vec::new()
560 }
561 }
562 }
563
564 #[must_use]
569 pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
570 self.rows_root = Some(RowsRoot {
571 rows: RefCell::new(Some(rows)),
572 once: false,
573 });
574 self
575 }
576
577 #[must_use]
585 pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
586 self.rows_root = Some(RowsRoot {
587 rows: RefCell::new(Some(rows)),
588 once: true,
589 });
590 self
591 }
592
593 pub fn take_root_failure(&self) -> Option<OqxError> {
598 self.root_failure.borrow_mut().take()
599 }
600
601 fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
604 self.statements_run.set(self.statements_run.get() + 1);
605 fetch_rows(self.conn, sql, params).map_err(sql_err)
606 }
607
608 fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
609 Ok(self.all(sql, params)?.into_iter().next())
610 }
611
612 fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
613 Ok(self
614 .one(sql, params)?
615 .and_then(|o| o.values().next().cloned())
616 .unwrap_or(Value::Undefined))
617 }
618
619 fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
620 self.statements_run.set(self.statements_run.get() + 1);
621 let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
622 stmt.exists(params_from_iter(params.iter()))
623 .map_err(sql_err)
624 }
625
626 fn tag_all(rows: Vec<Object>, t: Target) -> Value {
627 Value::Array(tag_rows(rows, t))
628 }
629
630 fn tag(row: Object, t: Target) -> Value {
631 tag_row(row, t)
632 }
633
634 fn root_row(&self) -> Value {
641 let mut o = Object::with_capacity(ALL_TARGETS.len());
642 for t in ALL_TARGETS {
643 o.insert(t.as_str(), scan_marker(t));
644 }
645 Value::Object(o)
646 }
647
648 fn root_scan(&self, t: Target) -> oqx::Result<Value> {
661 let repo = [SqlValue::Text(self.repo_id.clone())];
662 let sql = match t {
663 Target::Docs => {
664 "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
665 }
666 Target::Blocks => {
667 "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
668 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
669 ORDER BY d.path, b.block_id"
670 }
671 Target::Nodes => {
672 "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
673 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
674 }
675 Target::Links => {
676 "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
677 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
678 ORDER BY d.path, e.edge_id"
679 }
680 };
681 Ok(Self::tag_all(self.all(sql, &repo)?, t))
682 }
683
684 fn decode_prop(r: &Object) -> Value {
689 let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
690 match get("type").as_str().unwrap_or("") {
691 "number" => get("val_num"),
692 "bool" => Value::Bool(get("val_bool").truthy()),
693 "null" => Value::Null,
694 "json" => parse_json(&get("val_json")),
695 _ => get("val_text"),
696 }
697 }
698
699 fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
702 let rows = match source {
703 Some(s) => self.all(
704 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
705 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
706 )?,
707 None => self.all(
708 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
709 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
710 )?,
711 };
712 if rows.is_empty() {
713 return self.doc_prop_object(doc_id, key, source);
714 }
715 if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
716 return Ok(Self::decode_prop(&rows[0]));
717 }
718 Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
719 }
720
721 fn doc_prop_object(
724 &self,
725 doc_id: &str,
726 prefix: &str,
727 source: Option<&str>,
728 ) -> oqx::Result<Value> {
729 let like = SqlValue::Text(format!("{prefix}.%"));
730 let rows = match source {
731 Some(s) => self.all(
732 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
733 &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
734 )?,
735 None => self.all(
736 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
737 &[SqlValue::Text(doc_id.to_owned()), like],
738 )?,
739 };
740 if rows.is_empty() {
741 return Ok(Value::Undefined);
742 }
743 let mut out = Object::new();
744 for r in &rows {
745 let key = r.get("key").and_then(Value::as_str).unwrap_or("");
746 let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
747 .split('.')
748 .collect();
749 set_nested(&mut out, &rest, Self::decode_prop(r));
750 }
751 Ok(Value::Object(out))
752 }
753
754 fn doc_prop_bag(&self, doc_id: &str, source: Option<&str>) -> oqx::Result<Value> {
759 let keys = match source {
760 Some(s) => self.all(
761 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
762 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(s.to_owned())],
763 )?,
764 None => self.all(
765 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source IN ('frontmatter', 'inline') AND deleted_commit IS NULL ORDER BY key",
766 &[SqlValue::Text(doc_id.to_owned())],
767 )?,
768 };
769 let mut out = Object::new();
770 for k in keys {
771 let key = k.get("key").and_then(Value::as_str).unwrap_or("");
772 let top = key.split('.').next().unwrap_or("");
773 if !out.contains_key(top) {
774 let v = self.doc_prop(doc_id, top, source)?;
775 out.insert(top, v);
776 }
777 }
778 Ok(Value::Object(out))
779 }
780
781 fn doc_tags(&self, doc_id: &str) -> oqx::Result<Value> {
787 fn as_list(v: Value) -> Vec<Value> {
788 match v {
789 Value::Undefined | Value::Null => Vec::new(),
790 Value::Array(a) => a,
791 other => vec![other],
792 }
793 }
794 let authored = as_list(self.doc_prop(doc_id, "tags", None)?);
795 let hashtags = as_list(self.doc_prop(doc_id, "$tags", Some("computed"))?);
796 if authored.is_empty() && hashtags.is_empty() {
797 return Ok(Value::Null);
798 }
799 let mut seen: Vec<String> = Vec::new();
800 let mut out = Vec::new();
801 for v in authored.into_iter().chain(hashtags) {
802 let k = v.to_canonical_json().to_string();
803 if seen.contains(&k) {
804 continue;
805 }
806 seen.push(k);
807 out.push(v);
808 }
809 Ok(Value::Array(out))
810 }
811
812 fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
817 let ordinal = col(block, "ordinal").clone();
818 if col(block, "parent_block").is_absent() {
819 return Ok(ordinal);
820 }
821 let ap = col_str(block, "ancestor_path");
822 let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
823 return Ok(ordinal);
824 };
825 let r = self.scalar(
826 "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
827 &[
828 SqlValue::Text(col_str(block, "doc_id")),
829 SqlValue::Text(first.to_owned()),
830 ],
831 )?;
832 Ok(if r.is_absent() { ordinal } else { r })
833 }
834
835 fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
839 let rows = self.all(
840 "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",
841 &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
842 )?;
843 let ids: Vec<String> = rows
844 .iter()
845 .map(|r| {
846 r.get("block_id")
847 .and_then(Value::as_str)
848 .unwrap_or("")
849 .to_owned()
850 })
851 .collect();
852 let parent_index: Vec<Option<usize>> = rows
853 .iter()
854 .map(|r| {
855 r.get("parent_block")
856 .and_then(Value::as_str)
857 .and_then(|p| ids.iter().position(|id| id == p))
858 })
859 .collect();
860 let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
861 let mut roots = Vec::new();
862 for (i, p) in parent_index.iter().enumerate() {
863 match p {
864 Some(p) => children[*p].push(i),
865 None => roots.push(i),
866 }
867 }
868 fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
869 order.push(i);
870 for &c in &children[i] {
871 walk(c, children, order);
872 }
873 }
874 let mut order = Vec::with_capacity(rows.len());
875 for r in roots {
876 walk(r, &children, &mut order);
877 }
878 let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
879 Ok(order
880 .into_iter()
881 .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
882 .collect())
883 }
884
885 fn jattr(row: &Value, k: &str) -> Value {
886 match parse_json(col(row, "attrs")) {
887 Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
888 _ => Value::Undefined,
889 }
890 }
891
892 fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
894 let path = || SqlValue::Text(col_str(row, "__path"));
895 let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
896 let doc_path = || SqlValue::Text(col_str(row, "path"));
897 let block_id = || SqlValue::Text(col_str(row, "block_id"));
898 Ok(Some(match (t, key) {
899 (Target::Docs, "nodes") => {
900 let rows = self.all(
903 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
904 &[doc_path(), doc_id()],
905 )?;
906 let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
907 let rank: HashMap<String, usize> = blocks
908 .iter()
909 .enumerate()
910 .map(|(i, b)| (col_str(b, "block_id"), i))
911 .collect();
912 let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
913 .into_iter()
914 .map(|r| {
915 let block = r.get("block_id").and_then(Value::as_str);
916 let (has_block, rk) = match block {
917 None => (0, 0),
918 Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
919 };
920 let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
921 let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
922 ((has_block, rk, span, id), r)
923 })
924 .collect();
925 keyed.sort_by(|a, b| {
926 a.0.0
927 .cmp(&b.0.0)
928 .then(a.0.1.cmp(&b.0.1))
929 .then(a.0.2.total_cmp(&b.0.2))
930 .then(a.0.3.cmp(&b.0.3))
931 });
932 Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
933 }
934 (Target::Docs, "blocks") => {
935 Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
936 }
937 (Target::Docs, "out_links") => Self::tag_all(
942 self.all(
943 "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",
944 &[doc_path(), doc_id()],
945 )?,
946 Target::Links,
947 ),
948 (Target::Docs, "in_links") => Self::tag_all(
949 self.all(
950 "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
951 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
952 &[doc_id()],
953 )?,
954 Target::Links,
955 ),
956 (Target::Blocks, "children") => Self::tag_all(
957 self.all(
958 "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",
959 &[path(), block_id()],
960 )?,
961 Target::Blocks,
962 ),
963 (Target::Blocks, "nodes") => Self::tag_all(
964 self.all(
965 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
966 &[path(), block_id()],
967 )?,
968 Target::Nodes,
969 ),
970 (Target::Blocks, "out_links") => Self::tag_all(
971 self.all(
972 "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",
973 &[path(), block_id()],
974 )?,
975 Target::Links,
976 ),
977 (Target::Blocks, "section") => {
978 let top = to_sql(&self.top_ordinal(row)?);
979 Self::tag_all(
980 self.all(
981 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
982 AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
983 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
984 &[path(), doc_id(), top.clone(), top],
985 )?,
986 Target::Nodes,
987 )
988 }
989 (Target::Nodes, "blocks") => {
990 let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
991 if f.is_absent() || l.is_absent() {
992 return Ok(Some(Value::Array(Vec::new())));
993 }
994 let (f, l) = (
995 f.as_f64().unwrap_or(f64::NAN),
996 l.as_f64().unwrap_or(f64::NAN),
997 );
998 let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
999 let mut kept: Vec<Value> = Vec::new();
1000 for b in rows {
1001 let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
1002 if t >= f && t <= l {
1003 kept.push(b);
1004 }
1005 }
1006 Value::Array(kept)
1007 }
1008 (Target::Nodes, "subsections") => {
1009 let (f, l, lvl) = (
1010 Self::jattr(row, "first_ordinal"),
1011 Self::jattr(row, "last_ordinal"),
1012 Self::jattr(row, "level"),
1013 );
1014 if f.is_absent() {
1015 return Ok(Some(Value::Array(Vec::new())));
1016 }
1017 Self::tag_all(
1018 self.all(
1019 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
1020 AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
1021 AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1022 &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1023 )?,
1024 Target::Nodes,
1025 )
1026 }
1027 (Target::Nodes, "children") => {
1028 let (f, l, lvl) = (
1029 Self::jattr(row, "first_ordinal"),
1030 Self::jattr(row, "last_ordinal"),
1031 Self::jattr(row, "level"),
1032 );
1033 if f.is_absent() {
1034 return Ok(Some(Value::Array(Vec::new())));
1035 }
1036 Self::tag_all(
1037 self.all(
1038 "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
1039 AND json_extract(i.attrs,'$.level') > ?3
1040 AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
1041 AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
1042 AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
1043 AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
1044 AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
1045 ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
1046 &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1047 )?,
1048 Target::Nodes,
1049 )
1050 }
1051 _ => return Ok(None),
1052 }))
1053 }
1054
1055 fn lazy_owner(known: &[(&str, Value)], t: Target) -> Value {
1068 let mut o = Object::with_capacity(known.len() + 2);
1069 for (k, v) in known {
1070 o.insert(*k, v.clone());
1071 }
1072 o.insert(LAZY_KEY, Value::Bool(true));
1073 tag_row(o, t)
1074 }
1075
1076 fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
1077 let id = match col(row, "doc_id") {
1078 Value::Undefined | Value::Null => col(row, "src_doc").clone(),
1079 v => v.clone(),
1080 };
1081 if id.is_absent() {
1082 return Ok(absent_single());
1083 }
1084 match col(row, "__path") {
1085 Value::Str(path) => Ok(Self::lazy_owner(
1086 &[("doc_id", id), ("path", Value::Str(path.clone()))],
1087 Target::Docs,
1088 )),
1089 _ => Ok(self
1090 .load_owner(Target::Docs, &js_string(&id))?
1091 .unwrap_or_else(absent_single)),
1092 }
1093 }
1094
1095 fn destination_doc(&self, row: &Value) -> oqx::Result<Value> {
1102 if col(row, "dst_kind") != &Value::Str("document".to_owned()) {
1103 return Ok(absent_single());
1104 }
1105 Ok(self
1106 .load_owner(Target::Docs, &col_str(row, "dst_node"))?
1107 .unwrap_or_else(absent_single))
1108 }
1109
1110 fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
1111 let id = col(row, "block_id");
1112 if !id.truthy() {
1113 return Ok(absent_single());
1114 }
1115 match col(row, "__path") {
1116 Value::Str(path) => Ok(Self::lazy_owner(
1117 &[
1118 ("block_id", id.clone()),
1119 ("doc_id", col(row, "doc_id").clone()),
1120 ("__path", Value::Str(path.clone())),
1121 ],
1122 Target::Blocks,
1123 )),
1124 _ => Ok(self
1125 .load_owner(Target::Blocks, &js_string(id))?
1126 .unwrap_or_else(absent_single)),
1127 }
1128 }
1129
1130 fn load_owner(&self, t: Target, id: &str) -> oqx::Result<Option<Value>> {
1133 let key = (t, id.to_owned());
1134 if let Some(v) = self.owners.borrow().get(&key) {
1135 return Ok(v.clone());
1136 }
1137 let loaded = match t {
1138 Target::Docs => self
1139 .one(
1140 "SELECT * FROM docs WHERE doc_id = ?1",
1141 &[SqlValue::Text(id.to_owned())],
1142 )?
1143 .map(|o| Self::tag(o, Target::Docs)),
1144 _ => self
1145 .one(
1146 "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
1147 &[SqlValue::Text(id.to_owned())],
1148 )?
1149 .map(|o| Self::tag(o, Target::Blocks)),
1150 };
1151 self.owners.borrow_mut().insert(key, loaded.clone());
1152 Ok(loaded)
1153 }
1154
1155 fn lazy_needs_load(t: Target, key: &str) -> bool {
1165 match t {
1166 Target::Docs => matches!(key, "format" | "hash" | "updated_at"),
1167 Target::Blocks => {
1168 !matches!(
1169 key,
1170 "id" | "path"
1171 | "updated_at"
1172 | "$self"
1173 | "doc_id"
1174 | "block"
1175 | "doc"
1176 | "children"
1177 | "nodes"
1178 | "out_links"
1179 ) && Target::parse(key).is_none()
1180 }
1181 Target::Nodes | Target::Links => true,
1182 }
1183 }
1184
1185 fn resolve_lazy(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
1189 if !is_lazy_owner(row) || !Self::lazy_needs_load(t, key) {
1190 return Ok(None);
1191 }
1192 let id_col = if t == Target::Docs {
1193 "doc_id"
1194 } else {
1195 "block_id"
1196 };
1197 self.load_owner(t, &col_str(row, id_col))
1198 }
1199
1200 fn null_if_absent(v: Value) -> Value {
1203 if v.is_absent() { Value::Null } else { v }
1204 }
1205
1206 fn attribute(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
1215 let c = |k: &str| col(row, k).clone();
1216 let rooted = |k: &str| Value::Str(reference_path(&col_str(row, k)));
1217 Ok(match (t, name) {
1218 (Target::Docs, "id") => c("doc_id"),
1219 (Target::Docs, "path") => rooted("path"),
1220 (Target::Docs, "hash") => Self::null_if_absent(c("file_hash")),
1221 (Target::Docs, "updated_at") => Self::null_if_absent(self.scalar(
1222 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
1223 &[to_sql(&c("current_rev"))],
1224 )?),
1225 (Target::Docs, "raw") => {
1227 match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
1228 Ok(Some(s)) => Value::Str(s),
1229 Ok(None) => Value::Null,
1230 Err(e) => return Err(OqxError::eval(e.to_string())),
1231 }
1232 }
1233 (Target::Docs, "title") => {
1238 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
1239 }
1240 (Target::Docs, "tags") => self.doc_tags(&col_str(row, "doc_id"))?,
1243 (Target::Docs, "format") => c("format"),
1244 (Target::Blocks, "id") => c("block_id"),
1245 (Target::Blocks, "path") => rooted("__path"),
1246 (Target::Blocks, "raw") => match c("raw_hash") {
1250 Value::Str(hex) => {
1251 let hash = omgbase_store::tree::from_hex(&hex)
1252 .map_err(|e| OqxError::eval(e.to_string()))?;
1253 let bytes: Option<Vec<u8>> = self
1254 .conn
1255 .query_row(
1256 "SELECT bytes FROM blobs WHERE hash = ?1",
1257 rusqlite::params![hash],
1258 |r| r.get(0),
1259 )
1260 .optional()
1261 .map_err(sql_err)?;
1262 self.statements_run.set(self.statements_run.get() + 1);
1263 bytes.map_or(Value::Null, |b| {
1264 Value::Str(String::from_utf8_lossy(&b).into_owned())
1265 })
1266 }
1267 _ => Value::Null,
1268 },
1269 (Target::Blocks, "hash") => Self::null_if_absent(c("raw_hash")),
1270 (Target::Blocks, "updated_at") => Self::null_if_absent(self.scalar(
1271 "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
1272 &[to_sql(&c("block_id"))],
1273 )?),
1274 (Target::Blocks, "doc_id" | "type" | "text" | "ordinal" | "depth") => c(name),
1277 (Target::Blocks | Target::Nodes, "attrs") => parse_json(&c("attrs")),
1278 (Target::Nodes, "id") => c("node_id"),
1279 (Target::Nodes, "path") => rooted("__path"),
1280 (Target::Nodes, "doc_id" | "kind" | "name" | "value") => c(name),
1281 (Target::Nodes, "block_id") => Self::null_if_absent(c("block_id")),
1282 (Target::Links, "id") => c("edge_id"),
1285 (Target::Links, "path") => rooted("__path"),
1286 (Target::Links, "doc_id") => c("src_doc"),
1287 (Target::Links, "block_id") => Self::null_if_absent(c("src_block")),
1288 (Target::Links, "via_id") => Self::null_if_absent(c("via_node")),
1289 (Target::Links, "dst_id") => c("dst_node"),
1290 (Target::Links, "dst_path") => match self.scalar(
1291 "SELECT path FROM docs WHERE doc_id = ?1",
1292 &[to_sql(&c("dst_node"))],
1293 )? {
1294 Value::Undefined | Value::Null => Value::Null,
1295 p => Value::Str(reference_path(&js_string(&p))),
1296 },
1297 (Target::Links, "uri") => Self::null_if_absent(self.scalar(
1298 "SELECT uri FROM external_nodes WHERE node_id = ?1",
1299 &[to_sql(&c("dst_node"))],
1300 )?),
1301 (Target::Links, "field") => Self::null_if_absent(c("src_field")),
1302 (Target::Links, "predicate" | "provenance" | "dst_kind" | "anchor") => c(name),
1303 _ => Value::Undefined,
1304 })
1305 }
1306
1307 fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
1310 Some(Err(OqxError::eval(msg)))
1311 }
1312
1313 fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
1314 (t != want).then(|| {
1315 Err(OqxError::eval(format!(
1316 "{name}() is only available on the {} target",
1317 want.as_str()
1318 )))
1319 })
1320 }
1321
1322 fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
1323 r.map(Value::Bool)
1324 }
1325
1326 fn row_method(
1327 &self,
1328 name: &str,
1329 row: &Value,
1330 t: Target,
1331 args: &[Value],
1332 ) -> Option<oqx::Result<Value>> {
1333 let c = |k: &str| col(row, k).clone();
1334 match name {
1335 "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
1336 "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
1337 "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1338 Some(Self::sql_result(self.exists(
1339 "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
1340 &[to_sql(&c("block_id"))],
1341 )))
1342 }),
1343 "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1344 Some(self.scalar(
1345 "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
1346 &[to_sql(&c("block_id"))],
1347 ))
1348 }),
1349 "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1350 Some(
1351 self.scalar(
1352 "SELECT type FROM blocks WHERE block_id = ?1",
1353 &[to_sql(&c("parent_block"))],
1354 )
1355 .map(Self::null_if_absent),
1356 )
1357 }),
1358 "has_edge" => {
1359 let pred = js_string(args.first().unwrap_or(&Value::Undefined));
1360 let (src_col, src_val) = if t == Target::Blocks {
1361 ("src_block", c("block_id"))
1362 } else {
1363 ("src_doc", c("doc_id"))
1364 };
1365 let r = if args.len() >= 2 {
1366 self.exists(
1367 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
1368 &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
1369 )
1370 } else {
1371 self.exists(
1372 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
1373 &[to_sql(&src_val), SqlValue::Text(pred)],
1374 )
1375 };
1376 Some(Self::sql_result(r))
1377 }
1378 "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1379 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1380 let ap = col_str(row, "ancestor_path");
1381 Some(Ok(Value::Bool(
1382 ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
1383 )))
1384 }),
1385 "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1386 let text = js_string(args.first().unwrap_or(&Value::Undefined));
1387 let top = match self.top_ordinal(row) {
1388 Ok(v) => to_sql(&v),
1389 Err(e) => return Some(Err(e)),
1390 };
1391 Some(Self::sql_result(self.exists(
1392 "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
1393 WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
1394 &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
1395 )))
1396 }),
1397 "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1400 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1401 if target.starts_with("d_") {
1402 return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
1403 }
1404 let target = storage_path(&target).to_owned();
1405 if target.contains('*') {
1406 let like = glob_to_like(&target, false);
1407 return Some(Self::sql_result(self.exists(
1408 "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
1409 &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
1410 )));
1411 }
1412 Some(Ok(Value::Bool(col_str(row, "__path") == target)))
1413 }),
1414 "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1415 let kind = js_string(args.first().unwrap_or(&Value::Undefined));
1416 let ap: Vec<String> = col_str(row, "ancestor_path")
1417 .split('/')
1418 .filter(|s| !s.is_empty())
1419 .map(str::to_owned)
1420 .collect();
1421 if ap.is_empty() {
1422 return Some(Ok(Value::Bool(false)));
1423 }
1424 let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
1425 let placeholders = placeholders.join(",");
1426 let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
1427 let n = params.len();
1428 params.push(SqlValue::Text(kind));
1429 let r = match args.get(1) {
1430 Some(v) if !v.is_absent() => {
1431 let nm = js_string(v);
1432 params.push(SqlValue::Text(nm.clone()));
1433 params.push(SqlValue::Text(nm));
1434 self.exists(
1435 &format!(
1436 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
1437 n + 1,
1438 n + 2,
1439 n + 3
1440 ),
1441 ¶ms,
1442 )
1443 }
1444 _ => self.exists(
1445 &format!(
1446 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
1447 n + 1
1448 ),
1449 ¶ms,
1450 ),
1451 };
1452 Some(Self::sql_result(r))
1453 }),
1454 "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1455 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
1456 }),
1457 "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1458 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
1459 }),
1460 _ => None,
1461 }
1462 }
1463
1464 fn key_path(row: &Value, path: &str, kind: &str) -> Value {
1465 let key = if kind == "json" {
1466 let mut p = path;
1467 p = p.strip_prefix('#').unwrap_or(p);
1468 p = p.strip_prefix('/').unwrap_or(p);
1469 p.split('/').collect::<Vec<_>>().join(".")
1470 } else {
1471 path.to_owned()
1472 };
1473 let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
1474 if !col_str(row, "type").starts_with(&format!("{kind}:")) {
1475 return Value::Bool(false);
1476 }
1477 let k = Self::jattr(row, "key");
1478 Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
1479 }
1480
1481 fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
1482 if t == Target::Links {
1483 return Err(OqxError::eval(
1484 "text(...) is not available on the links target",
1485 ));
1486 }
1487 let m = sanitize_fts_query(terms);
1488 if m.is_empty() {
1489 return Ok(Value::Bool(false));
1490 }
1491 let r = match t {
1492 Target::Docs => self.exists(
1493 "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",
1494 &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1495 ),
1496 Target::Nodes => self.exists(
1497 "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1498 &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1499 ),
1500 _ => self.exists(
1501 "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1502 &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1503 ),
1504 };
1505 Self::sql_result(r)
1506 }
1507
1508 fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1518 let flags = match args.get(1) {
1519 None | Some(Value::Undefined) | Some(Value::Null) => None,
1520 Some(Value::Str(s)) => Some(s.clone()),
1521 Some(_) => return None,
1522 };
1523 let Some(subject) = string_form(recv) else {
1524 return Some(Ok(Value::Bool(false)));
1525 };
1526 let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1527 let key = (pattern, flags);
1528 let mut memo = self.regexes.borrow_mut();
1529 if !memo.contains_key(&key) {
1530 let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1531 match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1532 Ok(re) => {
1533 memo.insert(key.clone(), re);
1534 }
1535 Err(_) => return None,
1536 }
1537 }
1538 Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1539 }
1540
1541 fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1542 if matches!(t, Target::Nodes | Target::Links) {
1543 return Err(OqxError::eval(
1544 "semantic(...) is available on the docs and blocks targets",
1545 ));
1546 }
1547 let Some(resolved) = self.semantic.get(phrase) else {
1548 return Err(OqxError::eval(format!(
1549 "semantic({}) needs an embedding provider; none is configured for this query",
1550 serde_json::Value::String(phrase.to_owned())
1551 )));
1552 };
1553 let vec: oqx::Result<Option<Vec<u8>>> = match t {
1554 Target::Docs => self
1555 .conn
1556 .query_row(
1557 "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1558 rusqlite::params![col_str(row, "doc_id"), resolved.model],
1559 |r| r.get(0),
1560 )
1561 .optional()
1562 .map_err(sql_err),
1563 _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1566 .map_err(|e| OqxError::eval(e.to_string())),
1567 };
1568 match vec? {
1569 Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1570 None => Ok(Value::Null),
1571 }
1572 }
1573}
1574
1575pub(crate) fn fetch_rows(
1578 conn: &Connection,
1579 sql: &str,
1580 params: &[SqlValue],
1581) -> rusqlite::Result<Vec<Object>> {
1582 let mut stmt = conn.prepare_cached(sql)?;
1583 let names: Vec<String> = stmt
1584 .column_names()
1585 .iter()
1586 .map(|s| (*s).to_owned())
1587 .collect();
1588 let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1589 let mut o = Object::with_capacity(names.len() + 1);
1592 for (i, name) in names.iter().enumerate() {
1593 o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1594 }
1595 Ok(o)
1596 })?;
1597 rows.collect()
1598}
1599
1600pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1603 row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1604 Value::Object(row)
1605}
1606
1607pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1609 rows.into_iter().map(|r| tag_row(r, t)).collect()
1610}
1611
1612fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1615 let Some((first, rest)) = path.split_first() else {
1616 return;
1617 };
1618 if rest.is_empty() {
1619 out.insert(*first, leaf);
1620 return;
1621 }
1622 let mut child = match out.get(first) {
1623 Some(Value::Object(o)) => o.clone(),
1624 _ => Object::new(),
1625 };
1626 set_nested(&mut child, rest, leaf);
1627 out.insert(*first, Value::Object(child));
1628}
1629
1630#[must_use]
1633pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1634 let mut out = String::with_capacity(glob.len() + 4);
1635 for ch in glob.chars() {
1636 match ch {
1637 '%' | '_' => {
1638 out.push('\\');
1639 out.push(ch);
1640 }
1641 '\\' if escape_backslash => out.push_str("\\\\"),
1642 '*' => out.push('%'),
1643 c => out.push(c),
1644 }
1645 }
1646 out
1647}
1648
1649impl DataContext for StoreContext<'_> {
1650 fn root(&self, name: &str) -> Value {
1651 if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1652 let mut slot = rr.rows.borrow_mut();
1653 let rows = if rr.once { slot.take() } else { slot.clone() };
1654 return Value::Array(rows.unwrap_or_default());
1655 }
1656 if name == "id" {
1660 return Value::Str(self.repo_id.clone());
1661 }
1662 Target::parse(name).map_or(Value::Undefined, scan_marker)
1665 }
1666
1667 fn root_object(&self) -> Value {
1670 self.root_row()
1671 }
1672
1673 fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1674 if row.is_absent() {
1675 return Ok(Value::Undefined);
1676 }
1677 if is_root_object(row) {
1680 if key == "id" {
1681 return Ok(Value::Str(self.repo_id.clone()));
1682 }
1683 return Ok(Target::parse(key).map_or(Value::Undefined, scan_marker));
1684 }
1685 let Some(t) = target_of(row) else {
1686 return Ok(oqx::DefaultContext::read(row, key));
1688 };
1689 if let Some(full) = self.resolve_lazy(row, t, key)? {
1692 return self.get(&full, key);
1693 }
1694 if key == "$self" {
1699 return Ok(row.clone());
1700 }
1701 if attributes(t).contains(&key) {
1703 return self.attribute(row, t, key);
1704 }
1705 match (t, key) {
1707 (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1708 return Ok(row.clone());
1709 }
1710 (_, "doc") => return self.owning_doc(row),
1711 (Target::Nodes, "block") => return self.owning_block(row),
1712 (Target::Links, "dst") => return self.destination_doc(row),
1713 _ => {}
1714 }
1715 if let Some(v) = self.relation(row, t, key)? {
1716 return Ok(v);
1717 }
1718 if let Some(named) = Target::parse(key) {
1724 return Err(OqxError::eval(crate::query::bare_target_message(
1725 named, None,
1726 )));
1727 }
1728 Ok(match t {
1730 Target::Docs => {
1731 let doc_id = col_str(row, "doc_id");
1732 if SOURCE_HANDLES.contains(&key) {
1737 let source = (key != "data").then_some(key);
1738 return self.doc_prop_bag(&doc_id, source);
1739 }
1740 return self.doc_prop(&doc_id, key, None);
1741 }
1742 Target::Blocks | Target::Nodes => Self::jattr(row, key),
1746 Target::Links => Value::Undefined,
1747 })
1748 }
1749
1750 fn to_rows(&self, value: &Value) -> Vec<Value> {
1751 match value {
1752 Value::Undefined | Value::Null => Vec::new(),
1753 Value::Array(a) => a.clone(),
1754 other if is_absent_single(other) => Vec::new(),
1755 other => match scan_of(other) {
1756 Some(t) => self.expand_scan(t),
1757 None => vec![other.clone()],
1758 },
1759 }
1760 }
1761
1762 fn is_single(&self, value: &Value) -> bool {
1766 scan_of(value).is_none()
1767 }
1768
1769 fn materialize(&self, value: Value) -> Value {
1774 if value.as_object().is_some_and(|o| o.len() == 1) {
1775 if let Some(t) = scan_of(&value) {
1776 return Value::Array(self.expand_scan(t));
1777 }
1778 if is_absent_single(&value) {
1779 return Value::Undefined;
1780 }
1781 }
1782 value
1783 }
1784
1785 fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
1786 crate::store_index::index_for(self, scan_of(collection)?, path)
1787 }
1788
1789 fn identity(&self, row: &Value) -> Value {
1790 match target_of(row) {
1791 Some(Target::Docs) => col(row, "doc_id").clone(),
1792 Some(Target::Blocks) => col(row, "block_id").clone(),
1793 Some(Target::Nodes) => col(row, "node_id").clone(),
1794 Some(Target::Links) => col(row, "edge_id").clone(),
1795 None => row.clone(),
1796 }
1797 }
1798
1799 fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
1800 if name == "refs" {
1803 return Some(self.refs(args.first().unwrap_or(&Value::Undefined)));
1804 }
1805 if name == "range" {
1806 let x = args.first().unwrap_or(&Value::Undefined);
1807 return Some(Ok(match x {
1808 Value::Range(_) => x.clone(),
1809 Value::Str(s) => match omgbase_properties::detect_range(s) {
1810 Some(r) => {
1811 let b = |b: &Bound| match b {
1812 Bound::Open => Value::Undefined,
1813 Bound::Num(n) => Value::Number(*n),
1814 Bound::Iso(s) => Value::Str(s.clone()),
1815 };
1816 Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
1817 }
1818 None => Value::Null,
1819 },
1820 _ => Value::Null,
1821 }));
1822 }
1823 builtin_function(name, args)
1824 }
1825
1826 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1827 if let Some(t) = target_of(recv) {
1830 let full;
1832 let recv = if is_lazy_owner(recv) {
1833 let id_col = if t == Target::Docs {
1834 "doc_id"
1835 } else {
1836 "block_id"
1837 };
1838 match self.load_owner(t, &col_str(recv, id_col)) {
1839 Ok(Some(v)) => {
1840 full = v;
1841 &full
1842 }
1843 Ok(None) => recv,
1844 Err(e) => return Some(Err(e)),
1845 }
1846 } else {
1847 recv
1848 };
1849 if let Some(r) = self.row_method(name, recv, t, args) {
1850 return Some(r);
1851 }
1852 } else if matches!(
1853 name,
1854 "text"
1855 | "semantic"
1856 | "under"
1857 | "under_heading"
1858 | "within"
1859 | "under_kind"
1860 | "yaml_path"
1861 | "json_pointer"
1862 | "has_edge"
1863 | "has_anchor"
1864 | "child_count"
1865 | "parent_type"
1866 ) {
1867 return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/links row"));
1868 }
1869 if name == "matches" {
1870 if let Some(r) = self.matches_memoized(recv, args) {
1871 return Some(r);
1872 }
1873 }
1874 builtin_method_with(RegexDialect::Oqx, name, recv, args)
1875 }
1876}
1877
1878#[cfg(test)]
1879mod tests {
1880 use super::*;
1881
1882 #[test]
1883 fn glob_to_like_escapes() {
1884 assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
1885 assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
1886 assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
1887 }
1888
1889 #[test]
1890 fn rows_surfacing_as_values_render_id_and_path() {
1891 let mut node = Object::new();
1895 node.insert("node_id", Value::Str("n_1".into()));
1896 node.insert("attrs", Value::Str("{\"checked\":true}".into()));
1897 node.insert("__path", Value::Str("a.md".into()));
1898 let mut doc = Object::new();
1899 doc.insert("doc_id", Value::Str("d_0".into()));
1900 doc.insert("path", Value::Str("a.md".into()));
1901 doc.insert("blob", Value::Str("ff".into()));
1902 let mut record = Object::new();
1903 record.insert(TAG_KEY, Value::Str("junk".into()));
1904 record.insert(
1905 "tasks",
1906 Value::Array(vec![
1907 tag_row(node, Target::Nodes),
1908 tag_row(doc, Target::Docs),
1909 ]),
1910 );
1911 let out = render_row_values(Value::Object(record));
1912 let o = out.as_object().unwrap();
1913 assert!(o.get(TAG_KEY).is_none());
1914 let tasks = o.get("tasks").unwrap().as_array().unwrap();
1915 let keys = |v: &Value| -> Vec<String> {
1916 v.as_object()
1917 .unwrap()
1918 .iter()
1919 .map(|(k, _)| k.to_owned())
1920 .collect()
1921 };
1922 assert_eq!(keys(&tasks[0]), ["id", "path"]);
1923 assert_eq!(
1924 tasks[0].as_object().unwrap().get("id"),
1925 Some(&Value::Str("n_1".into()))
1926 );
1927 assert_eq!(
1928 tasks[0].as_object().unwrap().get("path"),
1929 Some(&Value::Str("/a.md".into()))
1930 );
1931 assert_eq!(keys(&tasks[1]), ["id", "path"]);
1932 assert_eq!(
1933 tasks[1].as_object().unwrap().get("id"),
1934 Some(&Value::Str("d_0".into()))
1935 );
1936 let mut edge = Object::new();
1938 edge.insert("edge_id", Value::Number(7.0));
1939 let e = render_row_values(tag_row(edge, Target::Links));
1940 assert_eq!(
1941 e.as_object().unwrap().get("id"),
1942 Some(&Value::Str("7".into()))
1943 );
1944 assert_eq!(
1945 e.as_object().unwrap().get("path"),
1946 Some(&Value::Str(String::new()))
1947 );
1948 }
1949
1950 #[test]
1951 fn nested_property_objects_rebuild() {
1952 let mut o = Object::new();
1953 set_nested(&mut o, &["a", "b"], Value::Number(1.0));
1954 set_nested(&mut o, &["a", "c"], Value::Number(2.0));
1955 set_nested(&mut o, &["d"], Value::Str("x".into()));
1956 let a = o.get("a").unwrap().as_object().unwrap();
1957 assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
1958 assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
1959 assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
1960 set_nested(&mut o, &["d", "e"], Value::Bool(true));
1962 assert!(o.get("d").unwrap().as_object().is_some());
1963 }
1964
1965 #[test]
1966 fn json_and_sql_bridges() {
1967 assert_eq!(
1968 parse_json(&Value::Str("{\"a\":1}".into()))
1969 .as_object()
1970 .unwrap()
1971 .get("a"),
1972 Some(&Value::Number(1.0))
1973 );
1974 assert_eq!(
1975 parse_json(&Value::Str("nope".into())),
1976 Value::Str("nope".into())
1977 );
1978 assert_eq!(parse_json(&Value::Null), Value::Undefined);
1979 assert_eq!(arg_or_empty(&[], 0), "");
1980 assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
1981 }
1982}