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::dates;
52use crate::paths::{reference_path, storage_path};
53use crate::planner;
54
55pub const TAG_KEY: &str = "__oqx_target";
57pub const SCAN_KEY: &str = "__oqx_scan";
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
76pub enum Target {
77 Docs,
78 Blocks,
79 Nodes,
80 Links,
81 Revisions,
82}
83
84impl Target {
85 #[must_use]
87 pub fn as_str(self) -> &'static str {
88 match self {
89 Target::Docs => "docs",
90 Target::Blocks => "blocks",
91 Target::Nodes => "nodes",
92 Target::Links => "links",
93 Target::Revisions => "revisions",
94 }
95 }
96
97 #[must_use]
99 pub fn parse(s: &str) -> Option<Self> {
100 Some(match s {
101 "docs" => Target::Docs,
102 "blocks" => Target::Blocks,
103 "nodes" => Target::Nodes,
104 "links" => Target::Links,
105 "revisions" => Target::Revisions,
106 _ => return None,
107 })
108 }
109
110 #[must_use]
113 pub fn noun(self) -> &'static str {
114 match self {
115 Target::Docs => "documents",
116 Target::Blocks => "blocks",
117 Target::Nodes => "nodes",
118 Target::Links => "links",
119 Target::Revisions => "revisions",
120 }
121 }
122}
123
124const DOC_ATTRIBUTES: [&str; 8] = [
146 "id",
147 "path",
148 "raw",
149 "hash",
150 "updated_at",
151 "title",
152 "tags",
153 "format",
154];
155const BLOCK_ATTRIBUTES: [&str; 11] = [
156 "id",
157 "path",
158 "raw",
159 "hash",
160 "updated_at",
161 "doc_id",
162 "type",
163 "text",
164 "attrs",
165 "depth",
166 "ordinal",
167];
168const NODE_ATTRIBUTES: [&str; 8] = [
169 "id", "path", "doc_id", "block_id", "kind", "name", "value", "attrs",
170];
171const EDGE_ATTRIBUTES: [&str; 13] = [
172 "id",
173 "path",
174 "doc_id",
175 "block_id",
176 "via_id",
177 "dst_id",
178 "dst_path",
179 "uri",
180 "field",
181 "dst_kind",
182 "predicate",
183 "provenance",
184 "anchor",
185];
186const REVISION_ATTRIBUTES: [&str; 13] = [
193 "id",
194 "path",
195 "rev",
196 "seq",
197 "doc_id",
198 "commit",
199 "ts",
200 "origin",
201 "actor",
202 "reason",
203 "hash",
204 "is_current",
205 "archived",
206];
207#[must_use]
209pub fn attributes(t: Target) -> &'static [&'static str] {
210 match t {
211 Target::Docs => &DOC_ATTRIBUTES,
212 Target::Blocks => &BLOCK_ATTRIBUTES,
213 Target::Nodes => &NODE_ATTRIBUTES,
214 Target::Links => &EDGE_ATTRIBUTES,
215 Target::Revisions => &REVISION_ATTRIBUTES,
216 }
217}
218
219pub(crate) const REVISION_COLS: &str = "r.rev_id, r.doc_id, r.seq, r.rendered_hash, r.path, r.commit_id, c.ts, c.origin, c.actor, c.reason, (d.current_rev = r.rev_id) AS is_current, (r.archive IS NOT NULL) AS archived";
227pub(crate) const REVISION_FROM: &str =
228 "revisions r JOIN docs d ON d.doc_id = r.doc_id JOIN commits c ON c.commit_id = r.commit_id";
229pub const SOURCE_HANDLES: [&str; 3] = ["data", "frontmatter", "inline"];
232
233pub const LAZY_KEY: &str = "__oqx_lazy";
247
248pub const ABSENT_KEY: &str = "__oqx_absent";
256
257fn absent_single() -> Value {
258 let mut o = Object::with_capacity(1);
259 o.insert(ABSENT_KEY, Value::Bool(true));
260 Value::Object(o)
261}
262
263fn is_absent_single(v: &Value) -> bool {
265 v.as_object()
266 .is_some_and(|o| o.len() == 1 && o.contains_key(ABSENT_KEY))
267}
268
269#[derive(Clone, Debug, PartialEq)]
272pub struct SemanticVec {
273 pub model: String,
274 pub vec: Vec<u8>,
275}
276
277pub struct StoreContext<'a> {
279 conn: &'a Connection,
280 repo_id: String,
281 semantic: HashMap<String, SemanticVec>,
282 root_failure: RefCell<Option<OqxError>>,
285 rows_root: Option<RowsRoot>,
287 regexes: RefCell<HashMap<(String, Option<String>), CompiledRegex>>,
290 scans: RefCell<HashMap<Target, Rc<Vec<Value>>>>,
294 scans_run: Cell<usize>,
296 statements_run: Cell<usize>,
299 owners: RefCell<HashMap<(Target, String), Option<Value>>>,
302 now_ms: i64,
307}
308
309struct RowsRoot {
314 rows: RefCell<Option<Vec<Value>>>,
315 once: bool,
316}
317
318fn sql_value(v: ValueRef<'_>) -> Value {
319 match v {
320 ValueRef::Null => Value::Null,
321 ValueRef::Integer(i) => Value::Number(i as f64),
322 ValueRef::Real(f) => Value::Number(f),
323 ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
324 ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
325 }
326}
327
328pub(crate) fn to_sql(v: &Value) -> SqlValue {
333 match v {
334 Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
335 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
336 Value::Number(n) => SqlValue::Real(*n),
337 Value::Str(s) => SqlValue::Text(s.clone()),
338 Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
339 }
340}
341
342fn js_string(v: &Value) -> String {
344 v.to_string()
345}
346
347fn arg_or_empty(args: &[Value], i: usize) -> String {
349 match args.get(i) {
350 None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
351 Some(v) => js_string(v),
352 }
353}
354
355fn parse_json(v: &Value) -> Value {
357 match v {
358 Value::Str(s) => {
359 serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
360 }
361 Value::Null | Value::Undefined => Value::Undefined,
362 other => other.clone(),
363 }
364}
365
366fn sql_err(e: rusqlite::Error) -> OqxError {
369 OqxError::eval(format!("sqlite: {e}"))
370}
371
372pub fn target_of(row: &Value) -> Option<Target> {
374 row.as_object()
375 .and_then(|o| o.get(TAG_KEY))
376 .and_then(Value::as_str)
377 .and_then(Target::parse)
378}
379
380fn is_lazy_owner(row: &Value) -> bool {
382 row.as_object().is_some_and(|o| o.contains_key(LAZY_KEY))
383}
384
385#[must_use]
387pub fn scan_of(v: &Value) -> Option<Target> {
388 v.as_object()
389 .and_then(|o| o.get(SCAN_KEY))
390 .and_then(Value::as_str)
391 .and_then(Target::parse)
392}
393
394fn scan_marker(t: Target) -> Value {
395 let mut o = Object::with_capacity(1);
396 o.insert(SCAN_KEY, Value::Str(t.as_str().to_owned()));
397 Value::Object(o)
398}
399
400pub const ALL_TARGETS: [Target; 5] = [
403 Target::Docs,
404 Target::Blocks,
405 Target::Nodes,
406 Target::Links,
407 Target::Revisions,
408];
409
410fn is_root_object(row: &Value) -> bool {
415 row.as_object().is_some_and(|o| {
416 o.len() == ALL_TARGETS.len()
417 && ALL_TARGETS
418 .iter()
419 .all(|t| o.get(t.as_str()).is_some_and(|v| scan_of(v) == Some(*t)))
420 })
421}
422
423fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
424 row.as_object()
425 .and_then(|o| o.get(key))
426 .unwrap_or(&Value::Undefined)
427}
428
429fn col_str(row: &Value, key: &str) -> String {
430 match col(row, key) {
431 Value::Undefined | Value::Null => String::new(),
432 v => js_string(v),
433 }
434}
435
436#[must_use]
438pub fn strip_tags(v: Value) -> Value {
439 match v {
440 Value::Object(o) => Value::Object(
441 o.into_iter()
442 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
443 .map(|(k, x)| (k, strip_tags(x)))
444 .collect(),
445 ),
446 Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
447 other => other,
448 }
449}
450
451#[must_use]
460pub fn render_row_values(v: Value) -> Value {
461 match v {
462 Value::Object(o) => {
463 let row = Value::Object(o);
464 if let Some(t) = target_of(&row) {
465 let (id_col, path_col) = match t {
466 Target::Docs => ("doc_id", "path"),
467 Target::Blocks => ("block_id", "__path"),
468 Target::Nodes => ("node_id", "__path"),
469 Target::Links => ("edge_id", "__path"),
470 Target::Revisions => ("rev_id", "path"),
473 };
474 let mut out = Object::with_capacity(2);
475 out.insert(crate::query::HIT_ID, Value::Str(col_str(&row, id_col)));
476 let path = match col(&row, path_col) {
477 Value::Undefined | Value::Null => String::new(),
478 v => reference_path(&js_string(v)),
479 };
480 out.insert(crate::query::HIT_PATH, Value::Str(path));
481 return Value::Object(out);
482 }
483 let Value::Object(o) = row else {
484 unreachable!()
485 };
486 Value::Object(
487 o.into_iter()
488 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
489 .map(|(k, x)| (k, render_row_values(x)))
490 .collect(),
491 )
492 }
493 Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
494 other => other,
495 }
496}
497
498impl<'a> StoreContext<'a> {
499 #[must_use]
502 pub fn new(
503 conn: &'a Connection,
504 repo_id: &str,
505 semantic: HashMap<String, SemanticVec>,
506 ) -> Self {
507 Self {
508 conn,
509 repo_id: repo_id.to_owned(),
510 semantic,
511 root_failure: RefCell::new(None),
512 rows_root: None,
513 regexes: RefCell::new(HashMap::new()),
514 scans: RefCell::new(HashMap::new()),
515 scans_run: Cell::new(0),
516 statements_run: Cell::new(0),
517 owners: RefCell::new(HashMap::new()),
518 now_ms: dates::wall_clock_ms(),
519 }
520 }
521
522 #[must_use]
526 pub fn with_now_ms(mut self, now_ms: i64) -> Self {
527 self.now_ms = now_ms;
528 self
529 }
530
531 #[must_use]
533 pub fn now_ms(&self) -> i64 {
534 self.now_ms
535 }
536
537 #[must_use]
540 pub fn statements_run(&self) -> usize {
541 self.statements_run.get()
542 }
543
544 #[must_use]
546 pub fn repo_id(&self) -> &str {
547 &self.repo_id
548 }
549
550 #[must_use]
554 pub fn scans_run(&self) -> usize {
555 self.scans_run.get()
556 }
557
558 pub(crate) fn scan_rows(&self, t: Target) -> oqx::Result<Rc<Vec<Value>>> {
560 if let Some(rows) = self.scans.borrow().get(&t) {
561 return Ok(Rc::clone(rows));
562 }
563 self.scans_run.set(self.scans_run.get() + 1);
564 let rows = match self.root_scan(t)? {
565 Value::Array(rows) => Rc::new(rows),
566 _ => Rc::new(Vec::new()),
567 };
568 self.scans.borrow_mut().insert(t, Rc::clone(&rows));
569 Ok(rows)
570 }
571
572 fn refs(&self, x: &Value) -> oqx::Result<Value> {
580 let items: Vec<&Value> = match x {
581 Value::Undefined | Value::Null => Vec::new(),
582 Value::Array(a) => a.iter().collect(),
583 other => vec![other],
584 };
585 let mut out = Vec::new();
586 for item in items {
587 if let Value::Str(s) = item
588 && let Some(row) = self.ref_doc(s)?
589 {
590 out.push(row);
591 }
592 }
593 Ok(Value::Array(out))
594 }
595
596 fn ref_doc(&self, r: &str) -> oqx::Result<Option<Value>> {
602 let t = Target::Docs;
603 let lookup = |column: &str, value: &str| -> oqx::Result<Option<Value>> {
604 let sql = format!(
605 "SELECT {} FROM {} WHERE {} AND d.{column} = ? ORDER BY {}",
606 planner::columns(t),
607 planner::from_clause(t),
608 planner::guards(t),
609 planner::order_clause(t)
610 );
611 let params = [
612 SqlValue::Text(self.repo_id.clone()),
613 SqlValue::Text(value.to_owned()),
614 ];
615 Ok(self.one(&sql, ¶ms)?.map(|row| tag_row(row, t)))
616 };
617 if let Some(row) = lookup("path", storage_path(r))? {
618 return Ok(Some(row));
619 }
620 if r.starts_with("d_") {
621 return lookup("doc_id", r);
622 }
623 Ok(None)
624 }
625
626 pub(crate) fn probe_rows(
628 &self,
629 t: Target,
630 sql: &str,
631 params: &[SqlValue],
632 ) -> oqx::Result<Vec<Value>> {
633 Ok(tag_rows(self.all(sql, params)?, t))
634 }
635
636 fn expand_scan(&self, t: Target) -> Vec<Value> {
639 match self.scan_rows(t) {
640 Ok(rows) => rows.as_ref().clone(),
641 Err(e) => {
642 let mut slot = self.root_failure.borrow_mut();
643 if slot.is_none() {
644 *slot = Some(e);
645 }
646 Vec::new()
647 }
648 }
649 }
650
651 #[must_use]
656 pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
657 self.rows_root = Some(RowsRoot {
658 rows: RefCell::new(Some(rows)),
659 once: false,
660 });
661 self
662 }
663
664 #[must_use]
672 pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
673 self.rows_root = Some(RowsRoot {
674 rows: RefCell::new(Some(rows)),
675 once: true,
676 });
677 self
678 }
679
680 pub fn take_root_failure(&self) -> Option<OqxError> {
685 self.root_failure.borrow_mut().take()
686 }
687
688 fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
691 self.statements_run.set(self.statements_run.get() + 1);
692 fetch_rows(self.conn, sql, params).map_err(sql_err)
693 }
694
695 fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
696 Ok(self.all(sql, params)?.into_iter().next())
697 }
698
699 fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
700 Ok(self
701 .one(sql, params)?
702 .and_then(|o| o.values().next().cloned())
703 .unwrap_or(Value::Undefined))
704 }
705
706 fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
707 self.statements_run.set(self.statements_run.get() + 1);
708 let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
709 stmt.exists(params_from_iter(params.iter()))
710 .map_err(sql_err)
711 }
712
713 fn tag_all(rows: Vec<Object>, t: Target) -> Value {
714 Value::Array(tag_rows(rows, t))
715 }
716
717 fn tag(row: Object, t: Target) -> Value {
718 tag_row(row, t)
719 }
720
721 fn root_row(&self) -> Value {
729 let mut o = Object::with_capacity(ALL_TARGETS.len());
730 for t in ALL_TARGETS {
731 o.insert(t.as_str(), scan_marker(t));
732 }
733 Value::Object(o)
734 }
735
736 fn root_scan(&self, t: Target) -> oqx::Result<Value> {
749 let repo = [SqlValue::Text(self.repo_id.clone())];
750 let sql = match t {
751 Target::Docs => {
752 "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
753 }
754 Target::Blocks => {
755 "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
756 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
757 ORDER BY d.path, b.block_id"
758 }
759 Target::Nodes => {
760 "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
761 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
762 }
763 Target::Links => {
764 "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
765 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
766 ORDER BY d.path, e.edge_id"
767 }
768 Target::Revisions => {
771 let sql = format!(
772 "SELECT {REVISION_COLS} FROM {REVISION_FROM} WHERE d.repo_id = ?1 ORDER BY r.path, r.rev_id"
773 );
774 return Ok(Self::tag_all(self.all(&sql, &repo)?, t));
775 }
776 };
777 Ok(Self::tag_all(self.all(sql, &repo)?, t))
778 }
779
780 fn doc_revisions(&self, doc_id: &str) -> oqx::Result<Value> {
783 let sql = format!(
784 "SELECT {REVISION_COLS} FROM {REVISION_FROM} WHERE r.doc_id = ?1 ORDER BY r.seq"
785 );
786 Ok(Self::tag_all(
787 self.all(&sql, &[SqlValue::Text(doc_id.to_owned())])?,
788 Target::Revisions,
789 ))
790 }
791
792 fn decode_prop(r: &Object) -> Value {
797 let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
798 match get("type").as_str().unwrap_or("") {
799 "number" => get("val_num"),
800 "bool" => Value::Bool(get("val_bool").truthy()),
801 "null" => Value::Null,
802 "json" => parse_json(&get("val_json")),
803 _ => get("val_text"),
804 }
805 }
806
807 fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
810 let rows = match source {
811 Some(s) => self.all(
812 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
813 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
814 )?,
815 None => self.all(
816 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
817 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
818 )?,
819 };
820 if rows.is_empty() {
821 return self.doc_prop_object(doc_id, key, source);
822 }
823 if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
824 return Ok(Self::decode_prop(&rows[0]));
825 }
826 Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
827 }
828
829 fn doc_prop_object(
832 &self,
833 doc_id: &str,
834 prefix: &str,
835 source: Option<&str>,
836 ) -> oqx::Result<Value> {
837 let like = SqlValue::Text(format!("{prefix}.%"));
838 let rows = match source {
839 Some(s) => self.all(
840 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
841 &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
842 )?,
843 None => self.all(
844 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
845 &[SqlValue::Text(doc_id.to_owned()), like],
846 )?,
847 };
848 if rows.is_empty() {
849 return Ok(Value::Undefined);
850 }
851 let mut out = Object::new();
852 for r in &rows {
853 let key = r.get("key").and_then(Value::as_str).unwrap_or("");
854 let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
855 .split('.')
856 .collect();
857 set_nested(&mut out, &rest, Self::decode_prop(r));
858 }
859 Ok(Value::Object(out))
860 }
861
862 fn doc_prop_bag(&self, doc_id: &str, source: Option<&str>) -> oqx::Result<Value> {
867 let keys = match source {
868 Some(s) => self.all(
869 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
870 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(s.to_owned())],
871 )?,
872 None => self.all(
873 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source IN ('frontmatter', 'inline') AND deleted_commit IS NULL ORDER BY key",
874 &[SqlValue::Text(doc_id.to_owned())],
875 )?,
876 };
877 let mut out = Object::new();
878 for k in keys {
879 let key = k.get("key").and_then(Value::as_str).unwrap_or("");
880 let top = key.split('.').next().unwrap_or("");
881 if !out.contains_key(top) {
882 let v = self.doc_prop(doc_id, top, source)?;
883 out.insert(top, v);
884 }
885 }
886 Ok(Value::Object(out))
887 }
888
889 fn doc_tags(&self, doc_id: &str) -> oqx::Result<Value> {
895 fn as_list(v: Value) -> Vec<Value> {
896 match v {
897 Value::Undefined | Value::Null => Vec::new(),
898 Value::Array(a) => a,
899 other => vec![other],
900 }
901 }
902 let authored = as_list(self.doc_prop(doc_id, "tags", None)?);
903 let hashtags = as_list(self.doc_prop(doc_id, "$tags", Some("computed"))?);
904 if authored.is_empty() && hashtags.is_empty() {
905 return Ok(Value::Null);
906 }
907 let mut seen: Vec<String> = Vec::new();
908 let mut out = Vec::new();
909 for v in authored.into_iter().chain(hashtags) {
910 let k = v.to_canonical_json().to_string();
911 if seen.contains(&k) {
912 continue;
913 }
914 seen.push(k);
915 out.push(v);
916 }
917 Ok(Value::Array(out))
918 }
919
920 fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
925 let ordinal = col(block, "ordinal").clone();
926 if col(block, "parent_block").is_absent() {
927 return Ok(ordinal);
928 }
929 let ap = col_str(block, "ancestor_path");
930 let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
931 return Ok(ordinal);
932 };
933 let r = self.scalar(
934 "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
935 &[
936 SqlValue::Text(col_str(block, "doc_id")),
937 SqlValue::Text(first.to_owned()),
938 ],
939 )?;
940 Ok(if r.is_absent() { ordinal } else { r })
941 }
942
943 fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
947 let rows = self.all(
948 "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",
949 &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
950 )?;
951 let ids: Vec<String> = rows
952 .iter()
953 .map(|r| {
954 r.get("block_id")
955 .and_then(Value::as_str)
956 .unwrap_or("")
957 .to_owned()
958 })
959 .collect();
960 let parent_index: Vec<Option<usize>> = rows
961 .iter()
962 .map(|r| {
963 r.get("parent_block")
964 .and_then(Value::as_str)
965 .and_then(|p| ids.iter().position(|id| id == p))
966 })
967 .collect();
968 let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
969 let mut roots = Vec::new();
970 for (i, p) in parent_index.iter().enumerate() {
971 match p {
972 Some(p) => children[*p].push(i),
973 None => roots.push(i),
974 }
975 }
976 fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
977 order.push(i);
978 for &c in &children[i] {
979 walk(c, children, order);
980 }
981 }
982 let mut order = Vec::with_capacity(rows.len());
983 for r in roots {
984 walk(r, &children, &mut order);
985 }
986 let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
987 Ok(order
988 .into_iter()
989 .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
990 .collect())
991 }
992
993 fn jattr(row: &Value, k: &str) -> Value {
994 match parse_json(col(row, "attrs")) {
995 Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
996 _ => Value::Undefined,
997 }
998 }
999
1000 fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
1002 let path = || SqlValue::Text(col_str(row, "__path"));
1003 let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
1004 let doc_path = || SqlValue::Text(col_str(row, "path"));
1005 let block_id = || SqlValue::Text(col_str(row, "block_id"));
1006 Ok(Some(match (t, key) {
1007 (Target::Docs, "nodes") => {
1008 let rows = self.all(
1011 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
1012 &[doc_path(), doc_id()],
1013 )?;
1014 let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
1015 let rank: HashMap<String, usize> = blocks
1016 .iter()
1017 .enumerate()
1018 .map(|(i, b)| (col_str(b, "block_id"), i))
1019 .collect();
1020 let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
1021 .into_iter()
1022 .map(|r| {
1023 let block = r.get("block_id").and_then(Value::as_str);
1024 let (has_block, rk) = match block {
1025 None => (0, 0),
1026 Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
1027 };
1028 let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
1029 let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
1030 ((has_block, rk, span, id), r)
1031 })
1032 .collect();
1033 keyed.sort_by(|a, b| {
1034 a.0.0
1035 .cmp(&b.0.0)
1036 .then(a.0.1.cmp(&b.0.1))
1037 .then(a.0.2.total_cmp(&b.0.2))
1038 .then(a.0.3.cmp(&b.0.3))
1039 });
1040 Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
1041 }
1042 (Target::Docs, "blocks") => {
1043 Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
1044 }
1045 (Target::Docs, "revisions") => self.doc_revisions(&col_str(row, "doc_id"))?,
1047 (Target::Docs, "out_links") => Self::tag_all(
1052 self.all(
1053 "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",
1054 &[doc_path(), doc_id()],
1055 )?,
1056 Target::Links,
1057 ),
1058 (Target::Docs, "in_links") => Self::tag_all(
1059 self.all(
1060 "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
1061 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
1062 &[doc_id()],
1063 )?,
1064 Target::Links,
1065 ),
1066 (Target::Blocks, "children") => Self::tag_all(
1067 self.all(
1068 "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",
1069 &[path(), block_id()],
1070 )?,
1071 Target::Blocks,
1072 ),
1073 (Target::Blocks, "nodes") => Self::tag_all(
1074 self.all(
1075 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
1076 &[path(), block_id()],
1077 )?,
1078 Target::Nodes,
1079 ),
1080 (Target::Blocks, "out_links") => Self::tag_all(
1081 self.all(
1082 "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",
1083 &[path(), block_id()],
1084 )?,
1085 Target::Links,
1086 ),
1087 (Target::Blocks, "section") => {
1088 let top = to_sql(&self.top_ordinal(row)?);
1089 Self::tag_all(
1090 self.all(
1091 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
1092 AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
1093 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1094 &[path(), doc_id(), top.clone(), top],
1095 )?,
1096 Target::Nodes,
1097 )
1098 }
1099 (Target::Nodes, "blocks") => {
1100 let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
1101 if f.is_absent() || l.is_absent() {
1102 return Ok(Some(Value::Array(Vec::new())));
1103 }
1104 let (f, l) = (
1105 f.as_f64().unwrap_or(f64::NAN),
1106 l.as_f64().unwrap_or(f64::NAN),
1107 );
1108 let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
1109 let mut kept: Vec<Value> = Vec::new();
1110 for b in rows {
1111 let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
1112 if t >= f && t <= l {
1113 kept.push(b);
1114 }
1115 }
1116 Value::Array(kept)
1117 }
1118 (Target::Nodes, "subsections") => {
1119 let (f, l, lvl) = (
1120 Self::jattr(row, "first_ordinal"),
1121 Self::jattr(row, "last_ordinal"),
1122 Self::jattr(row, "level"),
1123 );
1124 if f.is_absent() {
1125 return Ok(Some(Value::Array(Vec::new())));
1126 }
1127 Self::tag_all(
1128 self.all(
1129 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
1130 AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
1131 AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1132 &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1133 )?,
1134 Target::Nodes,
1135 )
1136 }
1137 (Target::Nodes, "children") => {
1138 let (f, l, lvl) = (
1139 Self::jattr(row, "first_ordinal"),
1140 Self::jattr(row, "last_ordinal"),
1141 Self::jattr(row, "level"),
1142 );
1143 if f.is_absent() {
1144 return Ok(Some(Value::Array(Vec::new())));
1145 }
1146 Self::tag_all(
1147 self.all(
1148 "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
1149 AND json_extract(i.attrs,'$.level') > ?3
1150 AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
1151 AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
1152 AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
1153 AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
1154 AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
1155 ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
1156 &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1157 )?,
1158 Target::Nodes,
1159 )
1160 }
1161 _ => return Ok(None),
1162 }))
1163 }
1164
1165 fn lazy_owner(known: &[(&str, Value)], t: Target) -> Value {
1178 let mut o = Object::with_capacity(known.len() + 2);
1179 for (k, v) in known {
1180 o.insert(*k, v.clone());
1181 }
1182 o.insert(LAZY_KEY, Value::Bool(true));
1183 tag_row(o, t)
1184 }
1185
1186 fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
1187 let id = match col(row, "doc_id") {
1188 Value::Undefined | Value::Null => col(row, "src_doc").clone(),
1189 v => v.clone(),
1190 };
1191 if id.is_absent() {
1192 return Ok(absent_single());
1193 }
1194 match col(row, "__path") {
1195 Value::Str(path) => Ok(Self::lazy_owner(
1196 &[("doc_id", id), ("path", Value::Str(path.clone()))],
1197 Target::Docs,
1198 )),
1199 _ => Ok(self
1200 .load_owner(Target::Docs, &js_string(&id))?
1201 .unwrap_or_else(absent_single)),
1202 }
1203 }
1204
1205 fn destination_doc(&self, row: &Value) -> oqx::Result<Value> {
1212 if col(row, "dst_kind") != &Value::Str("document".to_owned()) {
1213 return Ok(absent_single());
1214 }
1215 Ok(self
1216 .load_owner(Target::Docs, &col_str(row, "dst_node"))?
1217 .unwrap_or_else(absent_single))
1218 }
1219
1220 fn revision_doc(&self, row: &Value) -> oqx::Result<Value> {
1227 Ok(self
1228 .one(
1229 "SELECT * FROM docs WHERE doc_id = ?1 AND deleted_commit IS NULL",
1230 &[SqlValue::Text(col_str(row, "doc_id"))],
1231 )?
1232 .map_or_else(absent_single, |o| Self::tag(o, Target::Docs)))
1233 }
1234
1235 fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
1236 let id = col(row, "block_id");
1237 if !id.truthy() {
1238 return Ok(absent_single());
1239 }
1240 match col(row, "__path") {
1241 Value::Str(path) => Ok(Self::lazy_owner(
1242 &[
1243 ("block_id", id.clone()),
1244 ("doc_id", col(row, "doc_id").clone()),
1245 ("__path", Value::Str(path.clone())),
1246 ],
1247 Target::Blocks,
1248 )),
1249 _ => Ok(self
1250 .load_owner(Target::Blocks, &js_string(id))?
1251 .unwrap_or_else(absent_single)),
1252 }
1253 }
1254
1255 fn load_owner(&self, t: Target, id: &str) -> oqx::Result<Option<Value>> {
1258 let key = (t, id.to_owned());
1259 if let Some(v) = self.owners.borrow().get(&key) {
1260 return Ok(v.clone());
1261 }
1262 let loaded = match t {
1263 Target::Docs => self
1264 .one(
1265 "SELECT * FROM docs WHERE doc_id = ?1",
1266 &[SqlValue::Text(id.to_owned())],
1267 )?
1268 .map(|o| Self::tag(o, Target::Docs)),
1269 _ => self
1270 .one(
1271 "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
1272 &[SqlValue::Text(id.to_owned())],
1273 )?
1274 .map(|o| Self::tag(o, Target::Blocks)),
1275 };
1276 self.owners.borrow_mut().insert(key, loaded.clone());
1277 Ok(loaded)
1278 }
1279
1280 fn lazy_needs_load(t: Target, key: &str) -> bool {
1290 match t {
1291 Target::Docs => matches!(key, "format" | "hash" | "updated_at"),
1292 Target::Blocks => {
1293 !matches!(
1294 key,
1295 "id" | "path"
1296 | "updated_at"
1297 | "$self"
1298 | "doc_id"
1299 | "block"
1300 | "doc"
1301 | "children"
1302 | "nodes"
1303 | "out_links"
1304 ) && Target::parse(key).is_none()
1305 }
1306 Target::Nodes | Target::Links | Target::Revisions => true,
1308 }
1309 }
1310
1311 fn resolve_lazy(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
1315 if !is_lazy_owner(row) || !Self::lazy_needs_load(t, key) {
1316 return Ok(None);
1317 }
1318 let id_col = if t == Target::Docs {
1319 "doc_id"
1320 } else {
1321 "block_id"
1322 };
1323 self.load_owner(t, &col_str(row, id_col))
1324 }
1325
1326 fn null_if_absent(v: Value) -> Value {
1329 if v.is_absent() { Value::Null } else { v }
1330 }
1331
1332 fn attribute(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
1341 let c = |k: &str| col(row, k).clone();
1342 let rooted = |k: &str| Value::Str(reference_path(&col_str(row, k)));
1343 Ok(match (t, name) {
1344 (Target::Docs, "id") => c("doc_id"),
1345 (Target::Docs, "path") => rooted("path"),
1346 (Target::Docs, "hash") => Self::null_if_absent(c("file_hash")),
1347 (Target::Docs, "updated_at") => Self::null_if_absent(self.scalar(
1348 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
1349 &[to_sql(&c("current_rev"))],
1350 )?),
1351 (Target::Docs, "raw") => {
1353 match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
1354 Ok(Some(s)) => Value::Str(s),
1355 Ok(None) => Value::Null,
1356 Err(e) => return Err(OqxError::eval(e.to_string())),
1357 }
1358 }
1359 (Target::Docs, "title") => {
1364 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
1365 }
1366 (Target::Docs, "tags") => self.doc_tags(&col_str(row, "doc_id"))?,
1369 (Target::Docs, "format") => c("format"),
1370 (Target::Blocks, "id") => c("block_id"),
1371 (Target::Blocks, "path") => rooted("__path"),
1372 (Target::Blocks, "raw") => match c("raw_hash") {
1376 Value::Str(hex) => {
1377 let hash = omgbase_store::tree::from_hex(&hex)
1378 .map_err(|e| OqxError::eval(e.to_string()))?;
1379 let bytes: Option<Vec<u8>> = self
1380 .conn
1381 .query_row(
1382 "SELECT bytes FROM blobs WHERE hash = ?1",
1383 rusqlite::params![hash],
1384 |r| r.get(0),
1385 )
1386 .optional()
1387 .map_err(sql_err)?;
1388 self.statements_run.set(self.statements_run.get() + 1);
1389 bytes.map_or(Value::Null, |b| {
1390 Value::Str(String::from_utf8_lossy(&b).into_owned())
1391 })
1392 }
1393 _ => Value::Null,
1394 },
1395 (Target::Blocks, "hash") => Self::null_if_absent(c("raw_hash")),
1396 (Target::Blocks, "updated_at") => Self::null_if_absent(self.scalar(
1397 "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
1398 &[to_sql(&c("block_id"))],
1399 )?),
1400 (Target::Blocks, "doc_id" | "type" | "text" | "ordinal" | "depth") => c(name),
1403 (Target::Blocks | Target::Nodes, "attrs") => parse_json(&c("attrs")),
1404 (Target::Nodes, "id") => c("node_id"),
1405 (Target::Nodes, "path") => rooted("__path"),
1406 (Target::Nodes, "doc_id" | "kind" | "name" | "value") => c(name),
1407 (Target::Nodes, "block_id") => Self::null_if_absent(c("block_id")),
1408 (Target::Links, "id") => c("edge_id"),
1411 (Target::Links, "path") => rooted("__path"),
1412 (Target::Links, "doc_id") => c("src_doc"),
1413 (Target::Links, "block_id") => Self::null_if_absent(c("src_block")),
1414 (Target::Links, "via_id") => Self::null_if_absent(c("via_node")),
1415 (Target::Links, "dst_id") => c("dst_node"),
1416 (Target::Links, "dst_path") => match self.scalar(
1417 "SELECT path FROM docs WHERE doc_id = ?1",
1418 &[to_sql(&c("dst_node"))],
1419 )? {
1420 Value::Undefined | Value::Null => Value::Null,
1421 p => Value::Str(reference_path(&js_string(&p))),
1422 },
1423 (Target::Links, "uri") => Self::null_if_absent(self.scalar(
1424 "SELECT uri FROM external_nodes WHERE node_id = ?1",
1425 &[to_sql(&c("dst_node"))],
1426 )?),
1427 (Target::Links, "field") => Self::null_if_absent(c("src_field")),
1428 (Target::Links, "predicate" | "provenance" | "dst_kind" | "anchor") => c(name),
1429 (Target::Revisions, "id" | "rev") => c("rev_id"),
1433 (Target::Revisions, "path") => rooted("path"),
1434 (Target::Revisions, "seq" | "doc_id" | "ts" | "origin") => c(name),
1435 (Target::Revisions, "commit") => c("commit_id"),
1436 (Target::Revisions, "actor" | "reason") => Self::null_if_absent(c(name)),
1437 (Target::Revisions, "hash") => c("rendered_hash"),
1438 (Target::Revisions, "is_current" | "archived") => Value::Bool(matches!(
1439 c(name),
1440 Value::Number(n) if n == 1.0
1441 )),
1442 _ => Value::Undefined,
1443 })
1444 }
1445
1446 fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
1449 Some(Err(OqxError::eval(msg)))
1450 }
1451
1452 fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
1453 (t != want).then(|| {
1454 Err(OqxError::eval(format!(
1455 "{name}() is only available on the {} target",
1456 want.as_str()
1457 )))
1458 })
1459 }
1460
1461 fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
1462 r.map(Value::Bool)
1463 }
1464
1465 fn row_method(
1466 &self,
1467 name: &str,
1468 row: &Value,
1469 t: Target,
1470 args: &[Value],
1471 ) -> Option<oqx::Result<Value>> {
1472 let c = |k: &str| col(row, k).clone();
1473 match name {
1474 "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
1475 "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
1476 "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1477 Some(Self::sql_result(self.exists(
1478 "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
1479 &[to_sql(&c("block_id"))],
1480 )))
1481 }),
1482 "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1483 Some(self.scalar(
1484 "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
1485 &[to_sql(&c("block_id"))],
1486 ))
1487 }),
1488 "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1489 Some(
1490 self.scalar(
1491 "SELECT type FROM blocks WHERE block_id = ?1",
1492 &[to_sql(&c("parent_block"))],
1493 )
1494 .map(Self::null_if_absent),
1495 )
1496 }),
1497 "has_edge" => {
1498 let pred = js_string(args.first().unwrap_or(&Value::Undefined));
1499 let (src_col, src_val) = if t == Target::Blocks {
1500 ("src_block", c("block_id"))
1501 } else {
1502 ("src_doc", c("doc_id"))
1503 };
1504 let r = if args.len() >= 2 {
1505 self.exists(
1506 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
1507 &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
1508 )
1509 } else {
1510 self.exists(
1511 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
1512 &[to_sql(&src_val), SqlValue::Text(pred)],
1513 )
1514 };
1515 Some(Self::sql_result(r))
1516 }
1517 "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1518 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1519 let ap = col_str(row, "ancestor_path");
1520 Some(Ok(Value::Bool(
1521 ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
1522 )))
1523 }),
1524 "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1525 let text = js_string(args.first().unwrap_or(&Value::Undefined));
1526 let top = match self.top_ordinal(row) {
1527 Ok(v) => to_sql(&v),
1528 Err(e) => return Some(Err(e)),
1529 };
1530 Some(Self::sql_result(self.exists(
1531 "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
1532 WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
1533 &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
1534 )))
1535 }),
1536 "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1539 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1540 if target.starts_with("d_") {
1541 return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
1542 }
1543 let target = storage_path(&target).to_owned();
1544 if target.contains('*') {
1545 let like = glob_to_like(&target, false);
1546 return Some(Self::sql_result(self.exists(
1547 "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
1548 &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
1549 )));
1550 }
1551 Some(Ok(Value::Bool(col_str(row, "__path") == target)))
1552 }),
1553 "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1554 let kind = js_string(args.first().unwrap_or(&Value::Undefined));
1555 let ap: Vec<String> = col_str(row, "ancestor_path")
1556 .split('/')
1557 .filter(|s| !s.is_empty())
1558 .map(str::to_owned)
1559 .collect();
1560 if ap.is_empty() {
1561 return Some(Ok(Value::Bool(false)));
1562 }
1563 let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
1564 let placeholders = placeholders.join(",");
1565 let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
1566 let n = params.len();
1567 params.push(SqlValue::Text(kind));
1568 let r = match args.get(1) {
1569 Some(v) if !v.is_absent() => {
1570 let nm = js_string(v);
1571 params.push(SqlValue::Text(nm.clone()));
1572 params.push(SqlValue::Text(nm));
1573 self.exists(
1574 &format!(
1575 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
1576 n + 1,
1577 n + 2,
1578 n + 3
1579 ),
1580 ¶ms,
1581 )
1582 }
1583 _ => self.exists(
1584 &format!(
1585 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
1586 n + 1
1587 ),
1588 ¶ms,
1589 ),
1590 };
1591 Some(Self::sql_result(r))
1592 }),
1593 "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1594 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
1595 }),
1596 "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1597 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
1598 }),
1599 _ => None,
1600 }
1601 }
1602
1603 fn key_path(row: &Value, path: &str, kind: &str) -> Value {
1604 let key = if kind == "json" {
1605 let mut p = path;
1606 p = p.strip_prefix('#').unwrap_or(p);
1607 p = p.strip_prefix('/').unwrap_or(p);
1608 p.split('/').collect::<Vec<_>>().join(".")
1609 } else {
1610 path.to_owned()
1611 };
1612 let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
1613 if !col_str(row, "type").starts_with(&format!("{kind}:")) {
1614 return Value::Bool(false);
1615 }
1616 let k = Self::jattr(row, "key");
1617 Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
1618 }
1619
1620 fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
1621 if matches!(t, Target::Links | Target::Revisions) {
1623 return Err(OqxError::eval(format!(
1624 "text(...) is not available on the {} target",
1625 t.as_str()
1626 )));
1627 }
1628 let m = sanitize_fts_query(terms);
1629 if m.is_empty() {
1630 return Ok(Value::Bool(false));
1631 }
1632 let r = match t {
1633 Target::Docs => self.exists(
1634 "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",
1635 &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1636 ),
1637 Target::Nodes => self.exists(
1638 "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1639 &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1640 ),
1641 _ => self.exists(
1642 "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1643 &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1644 ),
1645 };
1646 Self::sql_result(r)
1647 }
1648
1649 fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1659 let flags = match args.get(1) {
1660 None | Some(Value::Undefined) | Some(Value::Null) => None,
1661 Some(Value::Str(s)) => Some(s.clone()),
1662 Some(_) => return None,
1663 };
1664 let Some(subject) = string_form(recv) else {
1665 return Some(Ok(Value::Bool(false)));
1666 };
1667 let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1668 let key = (pattern, flags);
1669 let mut memo = self.regexes.borrow_mut();
1670 if !memo.contains_key(&key) {
1671 let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1672 match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1673 Ok(re) => {
1674 memo.insert(key.clone(), re);
1675 }
1676 Err(_) => return None,
1677 }
1678 }
1679 Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1680 }
1681
1682 fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1683 if matches!(t, Target::Nodes | Target::Links | Target::Revisions) {
1684 return Err(OqxError::eval(
1685 "semantic(...) is available on the docs and blocks targets",
1686 ));
1687 }
1688 let Some(resolved) = self.semantic.get(phrase) else {
1689 return Err(OqxError::eval(format!(
1690 "semantic({}) needs an embedding provider; none is configured for this query",
1691 serde_json::Value::String(phrase.to_owned())
1692 )));
1693 };
1694 let vec: oqx::Result<Option<Vec<u8>>> = match t {
1695 Target::Docs => self
1696 .conn
1697 .query_row(
1698 "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1699 rusqlite::params![col_str(row, "doc_id"), resolved.model],
1700 |r| r.get(0),
1701 )
1702 .optional()
1703 .map_err(sql_err),
1704 _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1707 .map_err(|e| OqxError::eval(e.to_string())),
1708 };
1709 match vec? {
1710 Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1711 None => Ok(Value::Null),
1712 }
1713 }
1714}
1715
1716pub(crate) fn fetch_rows(
1719 conn: &Connection,
1720 sql: &str,
1721 params: &[SqlValue],
1722) -> rusqlite::Result<Vec<Object>> {
1723 let mut stmt = conn.prepare_cached(sql)?;
1724 let names: Vec<String> = stmt
1725 .column_names()
1726 .iter()
1727 .map(|s| (*s).to_owned())
1728 .collect();
1729 let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1730 let mut o = Object::with_capacity(names.len() + 1);
1733 for (i, name) in names.iter().enumerate() {
1734 o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1735 }
1736 Ok(o)
1737 })?;
1738 rows.collect()
1739}
1740
1741pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1744 row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1745 Value::Object(row)
1746}
1747
1748pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1750 rows.into_iter().map(|r| tag_row(r, t)).collect()
1751}
1752
1753fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1756 let Some((first, rest)) = path.split_first() else {
1757 return;
1758 };
1759 if rest.is_empty() {
1760 out.insert(*first, leaf);
1761 return;
1762 }
1763 let mut child = match out.get(first) {
1764 Some(Value::Object(o)) => o.clone(),
1765 _ => Object::new(),
1766 };
1767 set_nested(&mut child, rest, leaf);
1768 out.insert(*first, Value::Object(child));
1769}
1770
1771#[must_use]
1774pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1775 let mut out = String::with_capacity(glob.len() + 4);
1776 for ch in glob.chars() {
1777 match ch {
1778 '%' | '_' => {
1779 out.push('\\');
1780 out.push(ch);
1781 }
1782 '\\' if escape_backslash => out.push_str("\\\\"),
1783 '*' => out.push('%'),
1784 c => out.push(c),
1785 }
1786 }
1787 out
1788}
1789
1790impl DataContext for StoreContext<'_> {
1791 fn root(&self, name: &str) -> Value {
1792 if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1793 let mut slot = rr.rows.borrow_mut();
1794 let rows = if rr.once { slot.take() } else { slot.clone() };
1795 return Value::Array(rows.unwrap_or_default());
1796 }
1797 if name == "id" {
1801 return Value::Str(self.repo_id.clone());
1802 }
1803 Target::parse(name).map_or(Value::Undefined, scan_marker)
1806 }
1807
1808 fn root_object(&self) -> Value {
1811 self.root_row()
1812 }
1813
1814 fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1815 if row.is_absent() {
1816 return Ok(Value::Undefined);
1817 }
1818 if is_root_object(row) {
1821 if key == "id" {
1822 return Ok(Value::Str(self.repo_id.clone()));
1823 }
1824 return Ok(Target::parse(key).map_or(Value::Undefined, scan_marker));
1825 }
1826 let Some(t) = target_of(row) else {
1827 return Ok(oqx::DefaultContext::read(row, key));
1829 };
1830 if let Some(full) = self.resolve_lazy(row, t, key)? {
1833 return self.get(&full, key);
1834 }
1835 if key == "$self" {
1840 return Ok(row.clone());
1841 }
1842 if attributes(t).contains(&key) {
1844 return self.attribute(row, t, key);
1845 }
1846 match (t, key) {
1848 (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1849 return Ok(row.clone());
1850 }
1851 (Target::Revisions, "doc") => return self.revision_doc(row),
1854 (_, "doc") => return self.owning_doc(row),
1855 (Target::Nodes, "block") => return self.owning_block(row),
1856 (Target::Links, "dst") => return self.destination_doc(row),
1857 _ => {}
1858 }
1859 if let Some(v) = self.relation(row, t, key)? {
1860 return Ok(v);
1861 }
1862 if let Some(named) = Target::parse(key) {
1869 return Err(OqxError::eval(crate::query::bare_target_message(
1870 named, None,
1871 )));
1872 }
1873 Ok(match t {
1875 Target::Docs => {
1876 let doc_id = col_str(row, "doc_id");
1877 if SOURCE_HANDLES.contains(&key) {
1882 let source = (key != "data").then_some(key);
1883 return self.doc_prop_bag(&doc_id, source);
1884 }
1885 return self.doc_prop(&doc_id, key, None);
1886 }
1887 Target::Blocks | Target::Nodes => Self::jattr(row, key),
1891 Target::Links | Target::Revisions => Value::Undefined,
1895 })
1896 }
1897
1898 fn to_rows(&self, value: &Value) -> Vec<Value> {
1899 match value {
1900 Value::Undefined | Value::Null => Vec::new(),
1901 Value::Array(a) => a.clone(),
1902 other if is_absent_single(other) => Vec::new(),
1903 other => match scan_of(other) {
1904 Some(t) => self.expand_scan(t),
1905 None => vec![other.clone()],
1906 },
1907 }
1908 }
1909
1910 fn is_single(&self, value: &Value) -> bool {
1914 scan_of(value).is_none()
1915 }
1916
1917 fn materialize(&self, value: Value) -> Value {
1922 if value.as_object().is_some_and(|o| o.len() == 1) {
1923 if let Some(t) = scan_of(&value) {
1924 return Value::Array(self.expand_scan(t));
1925 }
1926 if is_absent_single(&value) {
1927 return Value::Undefined;
1928 }
1929 }
1930 value
1931 }
1932
1933 fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
1934 crate::store_index::index_for(self, scan_of(collection)?, path)
1935 }
1936
1937 fn identity(&self, row: &Value) -> Value {
1938 match target_of(row) {
1939 Some(Target::Docs) => col(row, "doc_id").clone(),
1940 Some(Target::Blocks) => col(row, "block_id").clone(),
1941 Some(Target::Nodes) => col(row, "node_id").clone(),
1942 Some(Target::Links) => col(row, "edge_id").clone(),
1943 Some(Target::Revisions) => col(row, "rev_id").clone(),
1944 None => row.clone(),
1945 }
1946 }
1947
1948 fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
1949 if name == "refs" {
1952 return Some(self.refs(args.first().unwrap_or(&Value::Undefined)));
1953 }
1954 if let Some(r) = dates::date_function(name, args, self.now_ms) {
1959 return Some(r);
1960 }
1961 if name == "range" {
1962 let x = args.first().unwrap_or(&Value::Undefined);
1963 return Some(Ok(match x {
1964 Value::Range(_) => x.clone(),
1965 Value::Str(s) => match omgbase_properties::detect_range(s) {
1966 Some(r) => {
1967 let b = |b: &Bound| match b {
1968 Bound::Open => Value::Undefined,
1969 Bound::Num(n) => Value::Number(*n),
1970 Bound::Iso(s) => Value::Str(s.clone()),
1971 };
1972 Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
1973 }
1974 None => Value::Null,
1975 },
1976 _ => Value::Null,
1977 }));
1978 }
1979 builtin_function(name, args)
1980 }
1981
1982 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1983 if let Some(t) = target_of(recv) {
1986 let full;
1988 let recv = if is_lazy_owner(recv) {
1989 let id_col = if t == Target::Docs {
1990 "doc_id"
1991 } else {
1992 "block_id"
1993 };
1994 match self.load_owner(t, &col_str(recv, id_col)) {
1995 Ok(Some(v)) => {
1996 full = v;
1997 &full
1998 }
1999 Ok(None) => recv,
2000 Err(e) => return Some(Err(e)),
2001 }
2002 } else {
2003 recv
2004 };
2005 if let Some(r) = self.row_method(name, recv, t, args) {
2006 return Some(r);
2007 }
2008 } else if matches!(
2009 name,
2010 "text"
2011 | "semantic"
2012 | "under"
2013 | "under_heading"
2014 | "within"
2015 | "under_kind"
2016 | "yaml_path"
2017 | "json_pointer"
2018 | "has_edge"
2019 | "has_anchor"
2020 | "child_count"
2021 | "parent_type"
2022 ) {
2023 return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/links row"));
2024 }
2025 if let Some(r) = dates::date_method(name, recv, args) {
2030 return Some(r);
2031 }
2032 if name == "matches" {
2033 if let Some(r) = self.matches_memoized(recv, args) {
2034 return Some(r);
2035 }
2036 }
2037 builtin_method_with(RegexDialect::Oqx, name, recv, args)
2038 }
2039}
2040
2041#[cfg(test)]
2042mod tests {
2043 use super::*;
2044
2045 #[test]
2046 fn glob_to_like_escapes() {
2047 assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
2048 assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
2049 assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
2050 }
2051
2052 #[test]
2053 fn rows_surfacing_as_values_render_id_and_path() {
2054 let mut node = Object::new();
2058 node.insert("node_id", Value::Str("n_1".into()));
2059 node.insert("attrs", Value::Str("{\"checked\":true}".into()));
2060 node.insert("__path", Value::Str("a.md".into()));
2061 let mut doc = Object::new();
2062 doc.insert("doc_id", Value::Str("d_0".into()));
2063 doc.insert("path", Value::Str("a.md".into()));
2064 doc.insert("blob", Value::Str("ff".into()));
2065 let mut record = Object::new();
2066 record.insert(TAG_KEY, Value::Str("junk".into()));
2067 record.insert(
2068 "tasks",
2069 Value::Array(vec![
2070 tag_row(node, Target::Nodes),
2071 tag_row(doc, Target::Docs),
2072 ]),
2073 );
2074 let out = render_row_values(Value::Object(record));
2075 let o = out.as_object().unwrap();
2076 assert!(o.get(TAG_KEY).is_none());
2077 let tasks = o.get("tasks").unwrap().as_array().unwrap();
2078 let keys = |v: &Value| -> Vec<String> {
2079 v.as_object()
2080 .unwrap()
2081 .iter()
2082 .map(|(k, _)| k.to_owned())
2083 .collect()
2084 };
2085 assert_eq!(keys(&tasks[0]), ["id", "path"]);
2086 assert_eq!(
2087 tasks[0].as_object().unwrap().get("id"),
2088 Some(&Value::Str("n_1".into()))
2089 );
2090 assert_eq!(
2091 tasks[0].as_object().unwrap().get("path"),
2092 Some(&Value::Str("/a.md".into()))
2093 );
2094 assert_eq!(keys(&tasks[1]), ["id", "path"]);
2095 assert_eq!(
2096 tasks[1].as_object().unwrap().get("id"),
2097 Some(&Value::Str("d_0".into()))
2098 );
2099 let mut edge = Object::new();
2101 edge.insert("edge_id", Value::Number(7.0));
2102 let e = render_row_values(tag_row(edge, Target::Links));
2103 assert_eq!(
2104 e.as_object().unwrap().get("id"),
2105 Some(&Value::Str("7".into()))
2106 );
2107 assert_eq!(
2108 e.as_object().unwrap().get("path"),
2109 Some(&Value::Str(String::new()))
2110 );
2111 }
2112
2113 #[test]
2114 fn nested_property_objects_rebuild() {
2115 let mut o = Object::new();
2116 set_nested(&mut o, &["a", "b"], Value::Number(1.0));
2117 set_nested(&mut o, &["a", "c"], Value::Number(2.0));
2118 set_nested(&mut o, &["d"], Value::Str("x".into()));
2119 let a = o.get("a").unwrap().as_object().unwrap();
2120 assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
2121 assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
2122 assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
2123 set_nested(&mut o, &["d", "e"], Value::Bool(true));
2125 assert!(o.get("d").unwrap().as_object().is_some());
2126 }
2127
2128 #[test]
2129 fn json_and_sql_bridges() {
2130 assert_eq!(
2131 parse_json(&Value::Str("{\"a\":1}".into()))
2132 .as_object()
2133 .unwrap()
2134 .get("a"),
2135 Some(&Value::Number(1.0))
2136 );
2137 assert_eq!(
2138 parse_json(&Value::Str("nope".into())),
2139 Value::Str("nope".into())
2140 );
2141 assert_eq!(parse_json(&Value::Null), Value::Undefined);
2142 assert_eq!(arg_or_empty(&[], 0), "");
2143 assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
2144 }
2145}