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; 9] = [
154 "_id",
155 "_path",
156 "_raw",
157 "_hash",
158 "_updated_at",
159 "_created_at",
160 "_title",
161 "_tags",
162 "_format",
163];
164const BLOCK_ATTRIBUTES: [&str; 12] = [
165 "_id",
166 "_path",
167 "_raw",
168 "_hash",
169 "_updated_at",
170 "_created_at",
171 "_doc_id",
172 "_type",
173 "_text",
174 "_attrs",
175 "_depth",
176 "_ordinal",
177];
178const NODE_ATTRIBUTES: [&str; 8] = [
179 "_id",
180 "_path",
181 "_doc_id",
182 "_block_id",
183 "_kind",
184 "_name",
185 "_value",
186 "_attrs",
187];
188const EDGE_ATTRIBUTES: [&str; 13] = [
189 "_id",
190 "_path",
191 "_doc_id",
192 "_block_id",
193 "_via_id",
194 "_dst_id",
195 "_dst_path",
196 "_uri",
197 "_field",
198 "_dst_kind",
199 "_predicate",
200 "_provenance",
201 "_anchor",
202];
203const REVISION_ATTRIBUTES: [&str; 13] = [
210 "_id",
211 "_path",
212 "_rev",
213 "_seq",
214 "_doc_id",
215 "_commit",
216 "_ts",
217 "_origin",
218 "_actor",
219 "_reason",
220 "_hash",
221 "_is_current",
222 "_archived",
223];
224#[must_use]
226pub fn attributes(t: Target) -> &'static [&'static str] {
227 match t {
228 Target::Docs => &DOC_ATTRIBUTES,
229 Target::Blocks => &BLOCK_ATTRIBUTES,
230 Target::Nodes => &NODE_ATTRIBUTES,
231 Target::Links => &EDGE_ATTRIBUTES,
232 Target::Revisions => &REVISION_ATTRIBUTES,
233 }
234}
235
236pub(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";
244pub(crate) const REVISION_FROM: &str =
245 "revisions r JOIN docs d ON d.doc_id = r.doc_id JOIN commits c ON c.commit_id = r.commit_id";
246pub const SOURCE_HANDLES: [&str; 3] = ["_data", "_frontmatter", "_inline"];
249
250#[must_use]
253pub fn handle_source(handle: &str) -> Option<&str> {
254 (handle != "_data").then(|| &handle[1..])
255}
256
257#[must_use]
262pub fn reach_throughs(t: Target) -> &'static [&'static str] {
263 match t {
264 Target::Docs => &[],
265 Target::Blocks | Target::Revisions => &["_doc"],
266 Target::Nodes => &["_doc", "_block"],
267 Target::Links => &["_doc", "_dst", "_block"],
268 }
269}
270
271pub const LAZY_KEY: &str = "__oqx_lazy";
285
286pub const ABSENT_KEY: &str = "__oqx_absent";
294
295fn absent_single() -> Value {
296 let mut o = Object::with_capacity(1);
297 o.insert(ABSENT_KEY, Value::Bool(true));
298 Value::Object(o)
299}
300
301fn is_absent_single(v: &Value) -> bool {
303 v.as_object()
304 .is_some_and(|o| o.len() == 1 && o.contains_key(ABSENT_KEY))
305}
306
307#[derive(Clone, Debug, PartialEq)]
310pub struct SemanticVec {
311 pub model: String,
312 pub vec: Vec<u8>,
313}
314
315pub struct StoreContext<'a> {
317 conn: &'a Connection,
318 repo_id: String,
319 semantic: HashMap<String, SemanticVec>,
320 root_failure: RefCell<Option<OqxError>>,
323 rows_root: Option<RowsRoot>,
325 regexes: RefCell<HashMap<(String, Option<String>), CompiledRegex>>,
328 scans: RefCell<HashMap<Target, Rc<Vec<Value>>>>,
332 scans_run: Cell<usize>,
334 statements_run: Cell<usize>,
337 owners: RefCell<HashMap<(Target, String), Option<Value>>>,
340 now_ms: i64,
345}
346
347struct RowsRoot {
352 rows: RefCell<Option<Vec<Value>>>,
353 once: bool,
354}
355
356fn sql_value(v: ValueRef<'_>) -> Value {
357 match v {
358 ValueRef::Null => Value::Null,
359 ValueRef::Integer(i) => Value::Number(i as f64),
360 ValueRef::Real(f) => Value::Number(f),
361 ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
362 ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
363 }
364}
365
366pub(crate) fn to_sql(v: &Value) -> SqlValue {
371 match v {
372 Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
373 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
374 Value::Number(n) => SqlValue::Real(*n),
375 Value::Str(s) => SqlValue::Text(s.clone()),
376 Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
377 }
378}
379
380fn js_string(v: &Value) -> String {
382 v.to_string()
383}
384
385fn arg_or_empty(args: &[Value], i: usize) -> String {
387 match args.get(i) {
388 None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
389 Some(v) => js_string(v),
390 }
391}
392
393fn parse_json(v: &Value) -> Value {
395 match v {
396 Value::Str(s) => {
397 serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
398 }
399 Value::Null | Value::Undefined => Value::Undefined,
400 other => other.clone(),
401 }
402}
403
404fn sql_err(e: rusqlite::Error) -> OqxError {
407 OqxError::eval(format!("sqlite: {e}"))
408}
409
410pub fn target_of(row: &Value) -> Option<Target> {
412 row.as_object()
413 .and_then(|o| o.get(TAG_KEY))
414 .and_then(Value::as_str)
415 .and_then(Target::parse)
416}
417
418fn is_lazy_owner(row: &Value) -> bool {
420 row.as_object().is_some_and(|o| o.contains_key(LAZY_KEY))
421}
422
423#[must_use]
425pub fn scan_of(v: &Value) -> Option<Target> {
426 v.as_object()
427 .and_then(|o| o.get(SCAN_KEY))
428 .and_then(Value::as_str)
429 .and_then(Target::parse)
430}
431
432fn scan_marker(t: Target) -> Value {
433 let mut o = Object::with_capacity(1);
434 o.insert(SCAN_KEY, Value::Str(t.as_str().to_owned()));
435 Value::Object(o)
436}
437
438pub const ALL_TARGETS: [Target; 5] = [
441 Target::Docs,
442 Target::Blocks,
443 Target::Nodes,
444 Target::Links,
445 Target::Revisions,
446];
447
448fn is_root_object(row: &Value) -> bool {
453 row.as_object().is_some_and(|o| {
454 o.len() == ALL_TARGETS.len()
455 && ALL_TARGETS
456 .iter()
457 .all(|t| o.get(t.as_str()).is_some_and(|v| scan_of(v) == Some(*t)))
458 })
459}
460
461fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
462 row.as_object()
463 .and_then(|o| o.get(key))
464 .unwrap_or(&Value::Undefined)
465}
466
467fn col_str(row: &Value, key: &str) -> String {
468 match col(row, key) {
469 Value::Undefined | Value::Null => String::new(),
470 v => js_string(v),
471 }
472}
473
474#[must_use]
476pub fn strip_tags(v: Value) -> Value {
477 match v {
478 Value::Object(o) => Value::Object(
479 o.into_iter()
480 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
481 .map(|(k, x)| (k, strip_tags(x)))
482 .collect(),
483 ),
484 Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
485 other => other,
486 }
487}
488
489#[must_use]
498pub fn render_row_values(v: Value) -> Value {
499 match v {
500 Value::Object(o) => {
501 let row = Value::Object(o);
502 if let Some(t) = target_of(&row) {
503 let (id_col, path_col) = match t {
504 Target::Docs => ("doc_id", "path"),
505 Target::Blocks => ("block_id", "__path"),
506 Target::Nodes => ("node_id", "__path"),
507 Target::Links => ("edge_id", "__path"),
508 Target::Revisions => ("rev_id", "path"),
511 };
512 let mut out = Object::with_capacity(2);
513 out.insert(crate::query::HIT_ID, Value::Str(col_str(&row, id_col)));
514 let path = match col(&row, path_col) {
515 Value::Undefined | Value::Null => String::new(),
516 v => reference_path(&js_string(v)),
517 };
518 out.insert(crate::query::HIT_PATH, Value::Str(path));
519 return Value::Object(out);
520 }
521 let Value::Object(o) = row else {
522 unreachable!()
523 };
524 Value::Object(
525 o.into_iter()
526 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
527 .map(|(k, x)| (k, render_row_values(x)))
528 .collect(),
529 )
530 }
531 Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
532 other => other,
533 }
534}
535
536impl<'a> StoreContext<'a> {
537 #[must_use]
540 pub fn new(
541 conn: &'a Connection,
542 repo_id: &str,
543 semantic: HashMap<String, SemanticVec>,
544 ) -> Self {
545 Self {
546 conn,
547 repo_id: repo_id.to_owned(),
548 semantic,
549 root_failure: RefCell::new(None),
550 rows_root: None,
551 regexes: RefCell::new(HashMap::new()),
552 scans: RefCell::new(HashMap::new()),
553 scans_run: Cell::new(0),
554 statements_run: Cell::new(0),
555 owners: RefCell::new(HashMap::new()),
556 now_ms: dates::wall_clock_ms(),
557 }
558 }
559
560 #[must_use]
564 pub fn with_now_ms(mut self, now_ms: i64) -> Self {
565 self.now_ms = now_ms;
566 self
567 }
568
569 #[must_use]
571 pub fn now_ms(&self) -> i64 {
572 self.now_ms
573 }
574
575 #[must_use]
578 pub fn statements_run(&self) -> usize {
579 self.statements_run.get()
580 }
581
582 #[must_use]
584 pub fn repo_id(&self) -> &str {
585 &self.repo_id
586 }
587
588 #[must_use]
592 pub fn scans_run(&self) -> usize {
593 self.scans_run.get()
594 }
595
596 pub(crate) fn scan_rows(&self, t: Target) -> oqx::Result<Rc<Vec<Value>>> {
598 if let Some(rows) = self.scans.borrow().get(&t) {
599 return Ok(Rc::clone(rows));
600 }
601 self.scans_run.set(self.scans_run.get() + 1);
602 let rows = match self.root_scan(t)? {
603 Value::Array(rows) => Rc::new(rows),
604 _ => Rc::new(Vec::new()),
605 };
606 self.scans.borrow_mut().insert(t, Rc::clone(&rows));
607 Ok(rows)
608 }
609
610 fn refs(&self, x: &Value) -> oqx::Result<Value> {
618 let items: Vec<&Value> = match x {
619 Value::Undefined | Value::Null => Vec::new(),
620 Value::Array(a) => a.iter().collect(),
621 other => vec![other],
622 };
623 let mut out = Vec::new();
624 for item in items {
625 if let Value::Str(s) = item
626 && let Some(row) = self.ref_doc(s)?
627 {
628 out.push(row);
629 }
630 }
631 Ok(Value::Array(out))
632 }
633
634 fn ref_doc(&self, r: &str) -> oqx::Result<Option<Value>> {
640 let t = Target::Docs;
641 let lookup = |column: &str, value: &str| -> oqx::Result<Option<Value>> {
642 let sql = format!(
643 "SELECT {} FROM {} WHERE {} AND d.{column} = ? ORDER BY {}",
644 planner::columns(t),
645 planner::from_clause(t),
646 planner::guards(t),
647 planner::order_clause(t)
648 );
649 let params = [
650 SqlValue::Text(self.repo_id.clone()),
651 SqlValue::Text(value.to_owned()),
652 ];
653 Ok(self.one(&sql, ¶ms)?.map(|row| tag_row(row, t)))
654 };
655 if let Some(row) = lookup("path", storage_path(r))? {
656 return Ok(Some(row));
657 }
658 if r.starts_with("d_") {
659 return lookup("doc_id", r);
660 }
661 Ok(None)
662 }
663
664 pub(crate) fn probe_rows(
666 &self,
667 t: Target,
668 sql: &str,
669 params: &[SqlValue],
670 ) -> oqx::Result<Vec<Value>> {
671 Ok(tag_rows(self.all(sql, params)?, t))
672 }
673
674 fn expand_scan(&self, t: Target) -> Vec<Value> {
677 match self.scan_rows(t) {
678 Ok(rows) => rows.as_ref().clone(),
679 Err(e) => {
680 let mut slot = self.root_failure.borrow_mut();
681 if slot.is_none() {
682 *slot = Some(e);
683 }
684 Vec::new()
685 }
686 }
687 }
688
689 #[must_use]
694 pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
695 self.rows_root = Some(RowsRoot {
696 rows: RefCell::new(Some(rows)),
697 once: false,
698 });
699 self
700 }
701
702 #[must_use]
710 pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
711 self.rows_root = Some(RowsRoot {
712 rows: RefCell::new(Some(rows)),
713 once: true,
714 });
715 self
716 }
717
718 pub fn take_root_failure(&self) -> Option<OqxError> {
723 self.root_failure.borrow_mut().take()
724 }
725
726 fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
729 self.statements_run.set(self.statements_run.get() + 1);
730 fetch_rows(self.conn, sql, params).map_err(sql_err)
731 }
732
733 fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
734 Ok(self.all(sql, params)?.into_iter().next())
735 }
736
737 fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
738 Ok(self
739 .one(sql, params)?
740 .and_then(|o| o.values().next().cloned())
741 .unwrap_or(Value::Undefined))
742 }
743
744 fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
745 self.statements_run.set(self.statements_run.get() + 1);
746 let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
747 stmt.exists(params_from_iter(params.iter()))
748 .map_err(sql_err)
749 }
750
751 fn tag_all(rows: Vec<Object>, t: Target) -> Value {
752 Value::Array(tag_rows(rows, t))
753 }
754
755 fn tag(row: Object, t: Target) -> Value {
756 tag_row(row, t)
757 }
758
759 fn root_row(&self) -> Value {
767 let mut o = Object::with_capacity(ALL_TARGETS.len());
768 for t in ALL_TARGETS {
769 o.insert(t.as_str(), scan_marker(t));
770 }
771 Value::Object(o)
772 }
773
774 fn root_scan(&self, t: Target) -> oqx::Result<Value> {
787 let repo = [SqlValue::Text(self.repo_id.clone())];
788 let sql = match t {
789 Target::Docs => {
790 "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
791 }
792 Target::Blocks => {
793 "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
794 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
795 ORDER BY d.path, b.block_id"
796 }
797 Target::Nodes => {
798 "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
799 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
800 }
801 Target::Links => {
802 "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
803 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
804 ORDER BY d.path, e.edge_id"
805 }
806 Target::Revisions => {
809 let sql = format!(
810 "SELECT {REVISION_COLS} FROM {REVISION_FROM} WHERE d.repo_id = ?1 ORDER BY r.path, r.rev_id"
811 );
812 return Ok(Self::tag_all(self.all(&sql, &repo)?, t));
813 }
814 };
815 Ok(Self::tag_all(self.all(sql, &repo)?, t))
816 }
817
818 fn doc_revisions(&self, doc_id: &str) -> oqx::Result<Value> {
821 let sql = format!(
822 "SELECT {REVISION_COLS} FROM {REVISION_FROM} WHERE r.doc_id = ?1 ORDER BY r.seq"
823 );
824 Ok(Self::tag_all(
825 self.all(&sql, &[SqlValue::Text(doc_id.to_owned())])?,
826 Target::Revisions,
827 ))
828 }
829
830 fn decode_prop(r: &Object) -> Value {
835 let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
836 match get("type").as_str().unwrap_or("") {
837 "number" => get("val_num"),
838 "bool" => Value::Bool(get("val_bool").truthy()),
839 "null" => Value::Null,
840 "json" => parse_json(&get("val_json")),
841 _ => get("val_text"),
842 }
843 }
844
845 fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
848 let rows = match source {
849 Some(s) => self.all(
850 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
851 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
852 )?,
853 None => self.all(
854 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
855 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
856 )?,
857 };
858 if rows.is_empty() {
859 return self.doc_prop_object(doc_id, key, source);
860 }
861 if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
862 return Ok(Self::decode_prop(&rows[0]));
863 }
864 Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
865 }
866
867 fn doc_prop_object(
870 &self,
871 doc_id: &str,
872 prefix: &str,
873 source: Option<&str>,
874 ) -> oqx::Result<Value> {
875 let like = SqlValue::Text(format!("{prefix}.%"));
876 let rows = match source {
877 Some(s) => self.all(
878 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
879 &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
880 )?,
881 None => self.all(
882 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
883 &[SqlValue::Text(doc_id.to_owned()), like],
884 )?,
885 };
886 if rows.is_empty() {
887 return Ok(Value::Undefined);
888 }
889 let mut out = Object::new();
890 for r in &rows {
891 let key = r.get("key").and_then(Value::as_str).unwrap_or("");
892 let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
893 .split('.')
894 .collect();
895 set_nested(&mut out, &rest, Self::decode_prop(r));
896 }
897 Ok(Value::Object(out))
898 }
899
900 fn doc_prop_bag(&self, doc_id: &str, source: Option<&str>) -> oqx::Result<Value> {
905 let keys = match source {
906 Some(s) => self.all(
907 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
908 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(s.to_owned())],
909 )?,
910 None => self.all(
911 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source IN ('frontmatter', 'inline') AND deleted_commit IS NULL ORDER BY key",
912 &[SqlValue::Text(doc_id.to_owned())],
913 )?,
914 };
915 let mut out = Object::new();
916 for k in keys {
917 let key = k.get("key").and_then(Value::as_str).unwrap_or("");
918 let top = key.split('.').next().unwrap_or("");
919 if !out.contains_key(top) {
920 let v = self.doc_prop(doc_id, top, source)?;
921 out.insert(top, v);
922 }
923 }
924 Ok(Value::Object(out))
925 }
926
927 fn doc_tags(&self, doc_id: &str) -> oqx::Result<Value> {
933 fn as_list(v: Value) -> Vec<Value> {
934 match v {
935 Value::Undefined | Value::Null => Vec::new(),
936 Value::Array(a) => a,
937 other => vec![other],
938 }
939 }
940 let authored = as_list(self.doc_prop(doc_id, "tags", None)?);
941 let hashtags = as_list(self.doc_prop(doc_id, "$tags", Some("computed"))?);
942 if authored.is_empty() && hashtags.is_empty() {
943 return Ok(Value::Null);
944 }
945 let mut seen: Vec<String> = Vec::new();
946 let mut out = Vec::new();
947 for v in authored.into_iter().chain(hashtags) {
948 let k = v.to_canonical_json().to_string();
949 if seen.contains(&k) {
950 continue;
951 }
952 seen.push(k);
953 out.push(v);
954 }
955 Ok(Value::Array(out))
956 }
957
958 fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
963 let ordinal = col(block, "ordinal").clone();
964 if col(block, "parent_block").is_absent() {
965 return Ok(ordinal);
966 }
967 let ap = col_str(block, "ancestor_path");
968 let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
969 return Ok(ordinal);
970 };
971 let r = self.scalar(
972 "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
973 &[
974 SqlValue::Text(col_str(block, "doc_id")),
975 SqlValue::Text(first.to_owned()),
976 ],
977 )?;
978 Ok(if r.is_absent() { ordinal } else { r })
979 }
980
981 fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
985 let rows = self.all(
986 "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",
987 &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
988 )?;
989 let ids: Vec<String> = rows
990 .iter()
991 .map(|r| {
992 r.get("block_id")
993 .and_then(Value::as_str)
994 .unwrap_or("")
995 .to_owned()
996 })
997 .collect();
998 let parent_index: Vec<Option<usize>> = rows
999 .iter()
1000 .map(|r| {
1001 r.get("parent_block")
1002 .and_then(Value::as_str)
1003 .and_then(|p| ids.iter().position(|id| id == p))
1004 })
1005 .collect();
1006 let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
1007 let mut roots = Vec::new();
1008 for (i, p) in parent_index.iter().enumerate() {
1009 match p {
1010 Some(p) => children[*p].push(i),
1011 None => roots.push(i),
1012 }
1013 }
1014 fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
1015 order.push(i);
1016 for &c in &children[i] {
1017 walk(c, children, order);
1018 }
1019 }
1020 let mut order = Vec::with_capacity(rows.len());
1021 for r in roots {
1022 walk(r, &children, &mut order);
1023 }
1024 let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
1025 Ok(order
1026 .into_iter()
1027 .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
1028 .collect())
1029 }
1030
1031 fn jattr(row: &Value, k: &str) -> Value {
1032 match parse_json(col(row, "attrs")) {
1033 Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
1034 _ => Value::Undefined,
1035 }
1036 }
1037
1038 fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
1040 let path = || SqlValue::Text(col_str(row, "__path"));
1041 let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
1042 let doc_path = || SqlValue::Text(col_str(row, "path"));
1043 let block_id = || SqlValue::Text(col_str(row, "block_id"));
1044 Ok(Some(match (t, key) {
1045 (Target::Docs, "_nodes") => {
1046 let rows = self.all(
1049 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
1050 &[doc_path(), doc_id()],
1051 )?;
1052 let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
1053 let rank: HashMap<String, usize> = blocks
1054 .iter()
1055 .enumerate()
1056 .map(|(i, b)| (col_str(b, "block_id"), i))
1057 .collect();
1058 let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
1059 .into_iter()
1060 .map(|r| {
1061 let block = r.get("block_id").and_then(Value::as_str);
1062 let (has_block, rk) = match block {
1063 None => (0, 0),
1064 Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
1065 };
1066 let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
1067 let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
1068 ((has_block, rk, span, id), r)
1069 })
1070 .collect();
1071 keyed.sort_by(|a, b| {
1072 a.0.0
1073 .cmp(&b.0.0)
1074 .then(a.0.1.cmp(&b.0.1))
1075 .then(a.0.2.total_cmp(&b.0.2))
1076 .then(a.0.3.cmp(&b.0.3))
1077 });
1078 Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
1079 }
1080 (Target::Docs, "_blocks") => {
1081 Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
1082 }
1083 (Target::Docs, "_revisions") => self.doc_revisions(&col_str(row, "doc_id"))?,
1085 (Target::Docs, "_out_links") => Self::tag_all(
1090 self.all(
1091 "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",
1092 &[doc_path(), doc_id()],
1093 )?,
1094 Target::Links,
1095 ),
1096 (Target::Docs, "_in_links") => Self::tag_all(
1097 self.all(
1098 "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
1099 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
1100 &[doc_id()],
1101 )?,
1102 Target::Links,
1103 ),
1104 (Target::Blocks, "_children") => Self::tag_all(
1105 self.all(
1106 "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",
1107 &[path(), block_id()],
1108 )?,
1109 Target::Blocks,
1110 ),
1111 (Target::Blocks, "_nodes") => Self::tag_all(
1112 self.all(
1113 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
1114 &[path(), block_id()],
1115 )?,
1116 Target::Nodes,
1117 ),
1118 (Target::Blocks, "_out_links") => Self::tag_all(
1119 self.all(
1120 "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",
1121 &[path(), block_id()],
1122 )?,
1123 Target::Links,
1124 ),
1125 (Target::Blocks, "_section") => {
1126 let top = to_sql(&self.top_ordinal(row)?);
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 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1132 &[path(), doc_id(), top.clone(), top],
1133 )?,
1134 Target::Nodes,
1135 )
1136 }
1137 (Target::Nodes, "_blocks") => {
1138 let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
1139 if f.is_absent() || l.is_absent() {
1140 return Ok(Some(Value::Array(Vec::new())));
1141 }
1142 let (f, l) = (
1143 f.as_f64().unwrap_or(f64::NAN),
1144 l.as_f64().unwrap_or(f64::NAN),
1145 );
1146 let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
1147 let mut kept: Vec<Value> = Vec::new();
1148 for b in rows {
1149 let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
1150 if t >= f && t <= l {
1151 kept.push(b);
1152 }
1153 }
1154 Value::Array(kept)
1155 }
1156 (Target::Nodes, "_subsections") => {
1157 let (f, l, lvl) = (
1158 Self::jattr(row, "first_ordinal"),
1159 Self::jattr(row, "last_ordinal"),
1160 Self::jattr(row, "level"),
1161 );
1162 if f.is_absent() {
1163 return Ok(Some(Value::Array(Vec::new())));
1164 }
1165 Self::tag_all(
1166 self.all(
1167 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
1168 AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
1169 AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1170 &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1171 )?,
1172 Target::Nodes,
1173 )
1174 }
1175 (Target::Nodes, "_children") => {
1176 let (f, l, lvl) = (
1177 Self::jattr(row, "first_ordinal"),
1178 Self::jattr(row, "last_ordinal"),
1179 Self::jattr(row, "level"),
1180 );
1181 if f.is_absent() {
1182 return Ok(Some(Value::Array(Vec::new())));
1183 }
1184 Self::tag_all(
1185 self.all(
1186 "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
1187 AND json_extract(i.attrs,'$.level') > ?3
1188 AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
1189 AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
1190 AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
1191 AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
1192 AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
1193 ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
1194 &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1195 )?,
1196 Target::Nodes,
1197 )
1198 }
1199 _ => return Ok(None),
1200 }))
1201 }
1202
1203 fn lazy_owner(known: &[(&str, Value)], t: Target) -> Value {
1217 let mut o = Object::with_capacity(known.len() + 2);
1218 for (k, v) in known {
1219 o.insert(*k, v.clone());
1220 }
1221 o.insert(LAZY_KEY, Value::Bool(true));
1222 tag_row(o, t)
1223 }
1224
1225 fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
1226 let id = match col(row, "doc_id") {
1227 Value::Undefined | Value::Null => col(row, "src_doc").clone(),
1228 v => v.clone(),
1229 };
1230 if id.is_absent() {
1231 return Ok(absent_single());
1232 }
1233 match col(row, "__path") {
1234 Value::Str(path) => Ok(Self::lazy_owner(
1235 &[("doc_id", id), ("path", Value::Str(path.clone()))],
1236 Target::Docs,
1237 )),
1238 _ => Ok(self
1239 .load_owner(Target::Docs, &js_string(&id))?
1240 .unwrap_or_else(absent_single)),
1241 }
1242 }
1243
1244 fn destination_doc(&self, row: &Value) -> oqx::Result<Value> {
1251 if col(row, "dst_kind") != &Value::Str("document".to_owned()) {
1252 return Ok(absent_single());
1253 }
1254 Ok(self
1255 .load_owner(Target::Docs, &col_str(row, "dst_node"))?
1256 .unwrap_or_else(absent_single))
1257 }
1258
1259 fn revision_doc(&self, row: &Value) -> oqx::Result<Value> {
1266 Ok(self
1267 .one(
1268 "SELECT * FROM docs WHERE doc_id = ?1 AND deleted_commit IS NULL",
1269 &[SqlValue::Text(col_str(row, "doc_id"))],
1270 )?
1271 .map_or_else(absent_single, |o| Self::tag(o, Target::Docs)))
1272 }
1273
1274 fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
1275 let id = col(row, "block_id");
1276 if !id.truthy() {
1277 return Ok(absent_single());
1278 }
1279 match col(row, "__path") {
1280 Value::Str(path) => Ok(Self::lazy_owner(
1281 &[
1282 ("block_id", id.clone()),
1283 ("doc_id", col(row, "doc_id").clone()),
1284 ("__path", Value::Str(path.clone())),
1285 ],
1286 Target::Blocks,
1287 )),
1288 _ => Ok(self
1289 .load_owner(Target::Blocks, &js_string(id))?
1290 .unwrap_or_else(absent_single)),
1291 }
1292 }
1293
1294 fn link_block(&self, row: &Value) -> oqx::Result<Value> {
1300 let id = col(row, "src_block");
1301 if !id.truthy() {
1302 return Ok(absent_single());
1303 }
1304 match col(row, "__path") {
1305 Value::Str(path) => Ok(Self::lazy_owner(
1306 &[
1307 ("block_id", id.clone()),
1308 ("doc_id", col(row, "src_doc").clone()),
1309 ("__path", Value::Str(path.clone())),
1310 ],
1311 Target::Blocks,
1312 )),
1313 _ => Ok(self
1314 .load_owner(Target::Blocks, &js_string(id))?
1315 .unwrap_or_else(absent_single)),
1316 }
1317 }
1318
1319 fn load_owner(&self, t: Target, id: &str) -> oqx::Result<Option<Value>> {
1322 let key = (t, id.to_owned());
1323 if let Some(v) = self.owners.borrow().get(&key) {
1324 return Ok(v.clone());
1325 }
1326 let loaded = match t {
1327 Target::Docs => self
1328 .one(
1329 "SELECT * FROM docs WHERE doc_id = ?1",
1330 &[SqlValue::Text(id.to_owned())],
1331 )?
1332 .map(|o| Self::tag(o, Target::Docs)),
1333 _ => self
1334 .one(
1335 "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
1336 &[SqlValue::Text(id.to_owned())],
1337 )?
1338 .map(|o| Self::tag(o, Target::Blocks)),
1339 };
1340 self.owners.borrow_mut().insert(key, loaded.clone());
1341 Ok(loaded)
1342 }
1343
1344 fn lazy_needs_load(t: Target, key: &str) -> bool {
1355 match t {
1356 Target::Docs => matches!(key, "_format" | "_hash" | "_updated_at"),
1357 Target::Blocks => {
1358 !matches!(
1359 key,
1360 "_id"
1361 | "_path"
1362 | "_updated_at"
1363 | "_created_at"
1364 | "_doc_id"
1365 | "_doc"
1366 | "_children"
1367 | "_nodes"
1368 | "_out_links"
1369 ) && Target::parse(key).is_none()
1370 }
1371 Target::Nodes | Target::Links | Target::Revisions => true,
1373 }
1374 }
1375
1376 fn resolve_lazy(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
1380 if !is_lazy_owner(row) || !Self::lazy_needs_load(t, key) {
1381 return Ok(None);
1382 }
1383 let id_col = if t == Target::Docs {
1384 "doc_id"
1385 } else {
1386 "block_id"
1387 };
1388 self.load_owner(t, &col_str(row, id_col))
1389 }
1390
1391 fn null_if_absent(v: Value) -> Value {
1394 if v.is_absent() { Value::Null } else { v }
1395 }
1396
1397 fn attribute(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
1406 let c = |k: &str| col(row, k).clone();
1407 let rooted = |k: &str| Value::Str(reference_path(&col_str(row, k)));
1408 let column = &name[1..];
1410 Ok(match (t, name) {
1411 (Target::Docs, "_id") => c("doc_id"),
1412 (Target::Docs, "_path") => rooted("path"),
1413 (Target::Docs, "_hash") => Self::null_if_absent(c("file_hash")),
1414 (Target::Docs, "_updated_at") => Self::null_if_absent(self.scalar(
1415 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
1416 &[to_sql(&c("current_rev"))],
1417 )?),
1418 (Target::Docs, "_created_at") => Self::null_if_absent(self.scalar(
1425 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.doc_id = ?1 AND r.seq = 1",
1426 &[to_sql(&c("doc_id"))],
1427 )?),
1428 (Target::Docs, "_raw") => {
1430 match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
1431 Ok(Some(s)) => Value::Str(s),
1432 Ok(None) => Value::Null,
1433 Err(e) => return Err(OqxError::eval(e.to_string())),
1434 }
1435 }
1436 (Target::Docs, "_title") => {
1441 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
1442 }
1443 (Target::Docs, "_tags") => self.doc_tags(&col_str(row, "doc_id"))?,
1446 (Target::Docs, "_format") => c("format"),
1447 (Target::Blocks, "_id") => c("block_id"),
1448 (Target::Blocks, "_path") => rooted("__path"),
1449 (Target::Blocks, "_raw") => match c("raw_hash") {
1453 Value::Str(hex) => {
1454 let hash = omgbase_store::tree::from_hex(&hex)
1455 .map_err(|e| OqxError::eval(e.to_string()))?;
1456 let bytes: Option<Vec<u8>> = self
1457 .conn
1458 .query_row(
1459 "SELECT bytes FROM blobs WHERE hash = ?1",
1460 rusqlite::params![hash],
1461 |r| r.get(0),
1462 )
1463 .optional()
1464 .map_err(sql_err)?;
1465 self.statements_run.set(self.statements_run.get() + 1);
1466 bytes.map_or(Value::Null, |b| {
1467 Value::Str(String::from_utf8_lossy(&b).into_owned())
1468 })
1469 }
1470 _ => Value::Null,
1471 },
1472 (Target::Blocks, "_hash") => Self::null_if_absent(c("raw_hash")),
1473 (Target::Blocks, "_updated_at") => Self::null_if_absent(self.scalar(
1474 "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
1475 &[to_sql(&c("block_id"))],
1476 )?),
1477 (Target::Blocks, "_created_at") => Self::null_if_absent(self.scalar(
1485 "SELECT MIN(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
1486 &[to_sql(&c("block_id"))],
1487 )?),
1488 (Target::Blocks, "_doc_id" | "_type" | "_text" | "_ordinal" | "_depth") => c(column),
1491 (Target::Blocks | Target::Nodes, "_attrs") => parse_json(&c("attrs")),
1492 (Target::Nodes, "_id") => c("node_id"),
1493 (Target::Nodes, "_path") => rooted("__path"),
1494 (Target::Nodes, "_doc_id" | "_kind" | "_name" | "_value") => c(column),
1495 (Target::Nodes, "_block_id") => Self::null_if_absent(c("block_id")),
1496 (Target::Links, "_id") => c("edge_id"),
1499 (Target::Links, "_path") => rooted("__path"),
1500 (Target::Links, "_doc_id") => c("src_doc"),
1501 (Target::Links, "_block_id") => Self::null_if_absent(c("src_block")),
1502 (Target::Links, "_via_id") => Self::null_if_absent(c("via_node")),
1503 (Target::Links, "_dst_id") => c("dst_node"),
1504 (Target::Links, "_dst_path") => match self.scalar(
1505 "SELECT path FROM docs WHERE doc_id = ?1",
1506 &[to_sql(&c("dst_node"))],
1507 )? {
1508 Value::Undefined | Value::Null => Value::Null,
1509 p => Value::Str(reference_path(&js_string(&p))),
1510 },
1511 (Target::Links, "_uri") => Self::null_if_absent(self.scalar(
1512 "SELECT uri FROM external_nodes WHERE node_id = ?1",
1513 &[to_sql(&c("dst_node"))],
1514 )?),
1515 (Target::Links, "_field") => Self::null_if_absent(c("src_field")),
1516 (Target::Links, "_predicate" | "_provenance" | "_dst_kind" | "_anchor") => c(column),
1517 (Target::Revisions, "_id" | "_rev") => c("rev_id"),
1521 (Target::Revisions, "_path") => rooted("path"),
1522 (Target::Revisions, "_seq" | "_doc_id" | "_ts" | "_origin") => c(column),
1523 (Target::Revisions, "_commit") => c("commit_id"),
1524 (Target::Revisions, "_actor" | "_reason") => Self::null_if_absent(c(column)),
1525 (Target::Revisions, "_hash") => c("rendered_hash"),
1526 (Target::Revisions, "_is_current" | "_archived") => Value::Bool(matches!(
1527 c(column),
1528 Value::Number(n) if n == 1.0
1529 )),
1530 _ => Value::Undefined,
1531 })
1532 }
1533
1534 fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
1537 Some(Err(OqxError::eval(msg)))
1538 }
1539
1540 fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
1541 (t != want).then(|| {
1542 Err(OqxError::eval(format!(
1543 "{name}() is only available on the {} target",
1544 want.as_str()
1545 )))
1546 })
1547 }
1548
1549 fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
1550 r.map(Value::Bool)
1551 }
1552
1553 fn row_method(
1554 &self,
1555 name: &str,
1556 row: &Value,
1557 t: Target,
1558 args: &[Value],
1559 ) -> Option<oqx::Result<Value>> {
1560 let c = |k: &str| col(row, k).clone();
1561 match name {
1562 "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
1563 "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
1564 "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1565 Some(Self::sql_result(self.exists(
1566 "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
1567 &[to_sql(&c("block_id"))],
1568 )))
1569 }),
1570 "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1571 Some(self.scalar(
1572 "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
1573 &[to_sql(&c("block_id"))],
1574 ))
1575 }),
1576 "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1577 Some(
1578 self.scalar(
1579 "SELECT type FROM blocks WHERE block_id = ?1",
1580 &[to_sql(&c("parent_block"))],
1581 )
1582 .map(Self::null_if_absent),
1583 )
1584 }),
1585 "has_edge" => {
1586 let pred = js_string(args.first().unwrap_or(&Value::Undefined));
1587 let (src_col, src_val) = if t == Target::Blocks {
1588 ("src_block", c("block_id"))
1589 } else {
1590 ("src_doc", c("doc_id"))
1591 };
1592 let r = if args.len() >= 2 {
1593 self.exists(
1594 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
1595 &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
1596 )
1597 } else {
1598 self.exists(
1599 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
1600 &[to_sql(&src_val), SqlValue::Text(pred)],
1601 )
1602 };
1603 Some(Self::sql_result(r))
1604 }
1605 "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1606 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1607 let ap = col_str(row, "ancestor_path");
1608 Some(Ok(Value::Bool(
1609 ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
1610 )))
1611 }),
1612 "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1613 let text = js_string(args.first().unwrap_or(&Value::Undefined));
1614 let top = match self.top_ordinal(row) {
1615 Ok(v) => to_sql(&v),
1616 Err(e) => return Some(Err(e)),
1617 };
1618 Some(Self::sql_result(self.exists(
1619 "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
1620 WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
1621 &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
1622 )))
1623 }),
1624 "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1627 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1628 if target.starts_with("d_") {
1629 return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
1630 }
1631 let target = storage_path(&target).to_owned();
1632 if target.contains('*') {
1633 let like = glob_to_like(&target, false);
1634 return Some(Self::sql_result(self.exists(
1635 "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
1636 &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
1637 )));
1638 }
1639 Some(Ok(Value::Bool(col_str(row, "__path") == target)))
1640 }),
1641 "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1642 let kind = js_string(args.first().unwrap_or(&Value::Undefined));
1643 let ap: Vec<String> = col_str(row, "ancestor_path")
1644 .split('/')
1645 .filter(|s| !s.is_empty())
1646 .map(str::to_owned)
1647 .collect();
1648 if ap.is_empty() {
1649 return Some(Ok(Value::Bool(false)));
1650 }
1651 let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
1652 let placeholders = placeholders.join(",");
1653 let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
1654 let n = params.len();
1655 params.push(SqlValue::Text(kind));
1656 let r = match args.get(1) {
1657 Some(v) if !v.is_absent() => {
1658 let nm = js_string(v);
1659 params.push(SqlValue::Text(nm.clone()));
1660 params.push(SqlValue::Text(nm));
1661 self.exists(
1662 &format!(
1663 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
1664 n + 1,
1665 n + 2,
1666 n + 3
1667 ),
1668 ¶ms,
1669 )
1670 }
1671 _ => self.exists(
1672 &format!(
1673 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
1674 n + 1
1675 ),
1676 ¶ms,
1677 ),
1678 };
1679 Some(Self::sql_result(r))
1680 }),
1681 "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1682 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
1683 }),
1684 "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1685 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
1686 }),
1687 _ => None,
1688 }
1689 }
1690
1691 fn key_path(row: &Value, path: &str, kind: &str) -> Value {
1692 let key = if kind == "json" {
1693 let mut p = path;
1694 p = p.strip_prefix('#').unwrap_or(p);
1695 p = p.strip_prefix('/').unwrap_or(p);
1696 p.split('/').collect::<Vec<_>>().join(".")
1697 } else {
1698 path.to_owned()
1699 };
1700 let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
1701 if !col_str(row, "type").starts_with(&format!("{kind}:")) {
1702 return Value::Bool(false);
1703 }
1704 let k = Self::jattr(row, "key");
1705 Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
1706 }
1707
1708 fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
1709 if matches!(t, Target::Links | Target::Revisions) {
1711 return Err(OqxError::eval(format!(
1712 "text(...) is not available on the {} target",
1713 t.as_str()
1714 )));
1715 }
1716 let m = sanitize_fts_query(terms);
1717 if m.is_empty() {
1718 return Ok(Value::Bool(false));
1719 }
1720 let r = match t {
1721 Target::Docs => self.exists(
1722 "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",
1723 &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1724 ),
1725 Target::Nodes => self.exists(
1726 "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1727 &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1728 ),
1729 _ => self.exists(
1730 "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1731 &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1732 ),
1733 };
1734 Self::sql_result(r)
1735 }
1736
1737 fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1747 let flags = match args.get(1) {
1748 None | Some(Value::Undefined) | Some(Value::Null) => None,
1749 Some(Value::Str(s)) => Some(s.clone()),
1750 Some(_) => return None,
1751 };
1752 let Some(subject) = string_form(recv) else {
1753 return Some(Ok(Value::Bool(false)));
1754 };
1755 let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1756 let key = (pattern, flags);
1757 let mut memo = self.regexes.borrow_mut();
1758 if !memo.contains_key(&key) {
1759 let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1760 match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1761 Ok(re) => {
1762 memo.insert(key.clone(), re);
1763 }
1764 Err(_) => return None,
1765 }
1766 }
1767 Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1768 }
1769
1770 fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1771 if matches!(t, Target::Nodes | Target::Links | Target::Revisions) {
1772 return Err(OqxError::eval(
1773 "semantic(...) is available on the docs and blocks targets",
1774 ));
1775 }
1776 let Some(resolved) = self.semantic.get(phrase) else {
1777 return Err(OqxError::eval(format!(
1778 "semantic({}) needs an embedding provider; none is configured for this query",
1779 serde_json::Value::String(phrase.to_owned())
1780 )));
1781 };
1782 let vec: oqx::Result<Option<Vec<u8>>> = match t {
1783 Target::Docs => self
1784 .conn
1785 .query_row(
1786 "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1787 rusqlite::params![col_str(row, "doc_id"), resolved.model],
1788 |r| r.get(0),
1789 )
1790 .optional()
1791 .map_err(sql_err),
1792 _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1795 .map_err(|e| OqxError::eval(e.to_string())),
1796 };
1797 match vec? {
1798 Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1799 None => Ok(Value::Null),
1800 }
1801 }
1802}
1803
1804pub(crate) fn fetch_rows(
1807 conn: &Connection,
1808 sql: &str,
1809 params: &[SqlValue],
1810) -> rusqlite::Result<Vec<Object>> {
1811 let mut stmt = conn.prepare_cached(sql)?;
1812 let names: Vec<String> = stmt
1813 .column_names()
1814 .iter()
1815 .map(|s| (*s).to_owned())
1816 .collect();
1817 let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1818 let mut o = Object::with_capacity(names.len() + 1);
1821 for (i, name) in names.iter().enumerate() {
1822 o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1823 }
1824 Ok(o)
1825 })?;
1826 rows.collect()
1827}
1828
1829pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1832 row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1833 Value::Object(row)
1834}
1835
1836pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1838 rows.into_iter().map(|r| tag_row(r, t)).collect()
1839}
1840
1841fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1844 let Some((first, rest)) = path.split_first() else {
1845 return;
1846 };
1847 if rest.is_empty() {
1848 out.insert(*first, leaf);
1849 return;
1850 }
1851 let mut child = match out.get(first) {
1852 Some(Value::Object(o)) => o.clone(),
1853 _ => Object::new(),
1854 };
1855 set_nested(&mut child, rest, leaf);
1856 out.insert(*first, Value::Object(child));
1857}
1858
1859#[must_use]
1862pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1863 let mut out = String::with_capacity(glob.len() + 4);
1864 for ch in glob.chars() {
1865 match ch {
1866 '%' | '_' => {
1867 out.push('\\');
1868 out.push(ch);
1869 }
1870 '\\' if escape_backslash => out.push_str("\\\\"),
1871 '*' => out.push('%'),
1872 c => out.push(c),
1873 }
1874 }
1875 out
1876}
1877
1878impl DataContext for StoreContext<'_> {
1879 fn root(&self, name: &str) -> Value {
1880 if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1881 let mut slot = rr.rows.borrow_mut();
1882 let rows = if rr.once { slot.take() } else { slot.clone() };
1883 return Value::Array(rows.unwrap_or_default());
1884 }
1885 if name == "_id" {
1889 return Value::Str(self.repo_id.clone());
1890 }
1891 Target::parse(name).map_or(Value::Undefined, scan_marker)
1894 }
1895
1896 fn root_object(&self) -> Value {
1899 self.root_row()
1900 }
1901
1902 fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1903 if row.is_absent() {
1904 return Ok(Value::Undefined);
1905 }
1906 if is_root_object(row) {
1909 if key == "_id" {
1910 return Ok(Value::Str(self.repo_id.clone()));
1911 }
1912 return Ok(Target::parse(key).map_or(Value::Undefined, scan_marker));
1913 }
1914 let Some(t) = target_of(row) else {
1915 return Ok(oqx::DefaultContext::read(row, key));
1917 };
1918 if let Some(full) = self.resolve_lazy(row, t, key)? {
1921 return self.get(&full, key);
1922 }
1923 if key.starts_with('_') {
1932 if attributes(t).contains(&key) {
1933 return self.attribute(row, t, key);
1934 }
1935 match (t, key) {
1940 (Target::Revisions, "_doc") => return self.revision_doc(row),
1941 (Target::Blocks | Target::Nodes | Target::Links, "_doc") => {
1942 return self.owning_doc(row);
1943 }
1944 (Target::Nodes, "_block") => return self.owning_block(row),
1945 (Target::Links, "_dst") => return self.destination_doc(row),
1946 (Target::Links, "_block") => return self.link_block(row),
1947 _ => {}
1948 }
1949 if let Some(v) = self.relation(row, t, key)? {
1950 return Ok(v);
1951 }
1952 if t == Target::Docs && SOURCE_HANDLES.contains(&key) {
1957 return self.doc_prop_bag(&col_str(row, "doc_id"), handle_source(key));
1958 }
1959 return Ok(Value::Undefined);
1962 }
1963 if let Some(named) = Target::parse(key) {
1971 return Err(OqxError::eval(crate::query::bare_target_message(
1972 named, None,
1973 )));
1974 }
1975 Ok(match t {
1977 Target::Docs => self.doc_prop(&col_str(row, "doc_id"), key, None)?,
1978 Target::Blocks | Target::Nodes => Self::jattr(row, key),
1981 Target::Links | Target::Revisions => Value::Undefined,
1985 })
1986 }
1987
1988 fn to_rows(&self, value: &Value) -> Vec<Value> {
1989 match value {
1990 Value::Undefined | Value::Null => Vec::new(),
1991 Value::Array(a) => a.clone(),
1992 other if is_absent_single(other) => Vec::new(),
1993 other => match scan_of(other) {
1994 Some(t) => self.expand_scan(t),
1995 None => vec![other.clone()],
1996 },
1997 }
1998 }
1999
2000 fn is_single(&self, value: &Value) -> bool {
2004 scan_of(value).is_none()
2005 }
2006
2007 fn materialize(&self, value: Value) -> Value {
2012 if value.as_object().is_some_and(|o| o.len() == 1) {
2013 if let Some(t) = scan_of(&value) {
2014 return Value::Array(self.expand_scan(t));
2015 }
2016 if is_absent_single(&value) {
2017 return Value::Undefined;
2018 }
2019 }
2020 value
2021 }
2022
2023 fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
2024 crate::store_index::index_for(self, scan_of(collection)?, path)
2025 }
2026
2027 fn identity(&self, row: &Value) -> Value {
2028 match target_of(row) {
2029 Some(Target::Docs) => col(row, "doc_id").clone(),
2030 Some(Target::Blocks) => col(row, "block_id").clone(),
2031 Some(Target::Nodes) => col(row, "node_id").clone(),
2032 Some(Target::Links) => col(row, "edge_id").clone(),
2033 Some(Target::Revisions) => col(row, "rev_id").clone(),
2034 None => row.clone(),
2035 }
2036 }
2037
2038 fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
2039 if name == "refs" {
2042 return Some(self.refs(args.first().unwrap_or(&Value::Undefined)));
2043 }
2044 if let Some(r) = dates::date_function(name, args, self.now_ms) {
2049 return Some(r);
2050 }
2051 if name == "range" {
2052 let x = args.first().unwrap_or(&Value::Undefined);
2053 return Some(Ok(match x {
2054 Value::Range(_) => x.clone(),
2055 Value::Str(s) => match omgbase_properties::detect_range(s) {
2056 Some(r) => {
2057 let b = |b: &Bound| match b {
2058 Bound::Open => Value::Undefined,
2059 Bound::Num(n) => Value::Number(*n),
2060 Bound::Iso(s) => Value::Str(s.clone()),
2061 };
2062 Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
2063 }
2064 None => Value::Null,
2065 },
2066 _ => Value::Null,
2067 }));
2068 }
2069 builtin_function(name, args)
2070 }
2071
2072 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
2073 if let Some(t) = target_of(recv) {
2076 let full;
2078 let recv = if is_lazy_owner(recv) {
2079 let id_col = if t == Target::Docs {
2080 "doc_id"
2081 } else {
2082 "block_id"
2083 };
2084 match self.load_owner(t, &col_str(recv, id_col)) {
2085 Ok(Some(v)) => {
2086 full = v;
2087 &full
2088 }
2089 Ok(None) => recv,
2090 Err(e) => return Some(Err(e)),
2091 }
2092 } else {
2093 recv
2094 };
2095 if let Some(r) = self.row_method(name, recv, t, args) {
2096 return Some(r);
2097 }
2098 } else if matches!(
2099 name,
2100 "text"
2101 | "semantic"
2102 | "under"
2103 | "under_heading"
2104 | "within"
2105 | "under_kind"
2106 | "yaml_path"
2107 | "json_pointer"
2108 | "has_edge"
2109 | "has_anchor"
2110 | "child_count"
2111 | "parent_type"
2112 ) {
2113 return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/links row"));
2114 }
2115 if let Some(r) = dates::date_method(name, recv, args) {
2120 return Some(r);
2121 }
2122 if name == "matches" {
2123 if let Some(r) = self.matches_memoized(recv, args) {
2124 return Some(r);
2125 }
2126 }
2127 builtin_method_with(RegexDialect::Oqx, name, recv, args)
2128 }
2129}
2130
2131#[cfg(test)]
2132mod tests {
2133 use super::*;
2134
2135 #[test]
2136 fn glob_to_like_escapes() {
2137 assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
2138 assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
2139 assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
2140 }
2141
2142 #[test]
2143 fn rows_surfacing_as_values_render_id_and_path() {
2144 let mut node = Object::new();
2148 node.insert("node_id", Value::Str("n_1".into()));
2149 node.insert("attrs", Value::Str("{\"checked\":true}".into()));
2150 node.insert("__path", Value::Str("a.md".into()));
2151 let mut doc = Object::new();
2152 doc.insert("doc_id", Value::Str("d_0".into()));
2153 doc.insert("path", Value::Str("a.md".into()));
2154 doc.insert("blob", Value::Str("ff".into()));
2155 let mut record = Object::new();
2156 record.insert(TAG_KEY, Value::Str("junk".into()));
2157 record.insert(
2158 "tasks",
2159 Value::Array(vec![
2160 tag_row(node, Target::Nodes),
2161 tag_row(doc, Target::Docs),
2162 ]),
2163 );
2164 let out = render_row_values(Value::Object(record));
2165 let o = out.as_object().unwrap();
2166 assert!(o.get(TAG_KEY).is_none());
2167 let tasks = o.get("tasks").unwrap().as_array().unwrap();
2168 let keys = |v: &Value| -> Vec<String> {
2169 v.as_object()
2170 .unwrap()
2171 .iter()
2172 .map(|(k, _)| k.to_owned())
2173 .collect()
2174 };
2175 assert_eq!(keys(&tasks[0]), ["_id", "_path"]);
2176 assert_eq!(
2177 tasks[0].as_object().unwrap().get("_id"),
2178 Some(&Value::Str("n_1".into()))
2179 );
2180 assert_eq!(
2181 tasks[0].as_object().unwrap().get("_path"),
2182 Some(&Value::Str("/a.md".into()))
2183 );
2184 assert_eq!(keys(&tasks[1]), ["_id", "_path"]);
2185 assert_eq!(
2186 tasks[1].as_object().unwrap().get("_id"),
2187 Some(&Value::Str("d_0".into()))
2188 );
2189 let mut edge = Object::new();
2191 edge.insert("edge_id", Value::Number(7.0));
2192 let e = render_row_values(tag_row(edge, Target::Links));
2193 assert_eq!(
2194 e.as_object().unwrap().get("_id"),
2195 Some(&Value::Str("7".into()))
2196 );
2197 assert_eq!(
2198 e.as_object().unwrap().get("_path"),
2199 Some(&Value::Str(String::new()))
2200 );
2201 }
2202
2203 #[test]
2204 fn nested_property_objects_rebuild() {
2205 let mut o = Object::new();
2206 set_nested(&mut o, &["a", "b"], Value::Number(1.0));
2207 set_nested(&mut o, &["a", "c"], Value::Number(2.0));
2208 set_nested(&mut o, &["d"], Value::Str("x".into()));
2209 let a = o.get("a").unwrap().as_object().unwrap();
2210 assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
2211 assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
2212 assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
2213 set_nested(&mut o, &["d", "e"], Value::Bool(true));
2215 assert!(o.get("d").unwrap().as_object().is_some());
2216 }
2217
2218 #[test]
2219 fn json_and_sql_bridges() {
2220 assert_eq!(
2221 parse_json(&Value::Str("{\"a\":1}".into()))
2222 .as_object()
2223 .unwrap()
2224 .get("a"),
2225 Some(&Value::Number(1.0))
2226 );
2227 assert_eq!(
2228 parse_json(&Value::Str("nope".into())),
2229 Value::Str("nope".into())
2230 );
2231 assert_eq!(parse_json(&Value::Null), Value::Undefined);
2232 assert_eq!(arg_or_empty(&[], 0), "");
2233 assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
2234 }
2235}