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] = [
148 "id",
149 "path",
150 "raw",
151 "hash",
152 "updated_at",
153 "created_at",
154 "title",
155 "tags",
156 "format",
157];
158const BLOCK_ATTRIBUTES: [&str; 12] = [
159 "id",
160 "path",
161 "raw",
162 "hash",
163 "updated_at",
164 "created_at",
165 "doc_id",
166 "type",
167 "text",
168 "attrs",
169 "depth",
170 "ordinal",
171];
172const NODE_ATTRIBUTES: [&str; 8] = [
173 "id", "path", "doc_id", "block_id", "kind", "name", "value", "attrs",
174];
175const EDGE_ATTRIBUTES: [&str; 13] = [
176 "id",
177 "path",
178 "doc_id",
179 "block_id",
180 "via_id",
181 "dst_id",
182 "dst_path",
183 "uri",
184 "field",
185 "dst_kind",
186 "predicate",
187 "provenance",
188 "anchor",
189];
190const REVISION_ATTRIBUTES: [&str; 13] = [
197 "id",
198 "path",
199 "rev",
200 "seq",
201 "doc_id",
202 "commit",
203 "ts",
204 "origin",
205 "actor",
206 "reason",
207 "hash",
208 "is_current",
209 "archived",
210];
211#[must_use]
213pub fn attributes(t: Target) -> &'static [&'static str] {
214 match t {
215 Target::Docs => &DOC_ATTRIBUTES,
216 Target::Blocks => &BLOCK_ATTRIBUTES,
217 Target::Nodes => &NODE_ATTRIBUTES,
218 Target::Links => &EDGE_ATTRIBUTES,
219 Target::Revisions => &REVISION_ATTRIBUTES,
220 }
221}
222
223pub(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";
231pub(crate) const REVISION_FROM: &str =
232 "revisions r JOIN docs d ON d.doc_id = r.doc_id JOIN commits c ON c.commit_id = r.commit_id";
233pub const SOURCE_HANDLES: [&str; 3] = ["data", "frontmatter", "inline"];
236
237pub const LAZY_KEY: &str = "__oqx_lazy";
251
252pub const ABSENT_KEY: &str = "__oqx_absent";
260
261fn absent_single() -> Value {
262 let mut o = Object::with_capacity(1);
263 o.insert(ABSENT_KEY, Value::Bool(true));
264 Value::Object(o)
265}
266
267fn is_absent_single(v: &Value) -> bool {
269 v.as_object()
270 .is_some_and(|o| o.len() == 1 && o.contains_key(ABSENT_KEY))
271}
272
273#[derive(Clone, Debug, PartialEq)]
276pub struct SemanticVec {
277 pub model: String,
278 pub vec: Vec<u8>,
279}
280
281pub struct StoreContext<'a> {
283 conn: &'a Connection,
284 repo_id: String,
285 semantic: HashMap<String, SemanticVec>,
286 root_failure: RefCell<Option<OqxError>>,
289 rows_root: Option<RowsRoot>,
291 regexes: RefCell<HashMap<(String, Option<String>), CompiledRegex>>,
294 scans: RefCell<HashMap<Target, Rc<Vec<Value>>>>,
298 scans_run: Cell<usize>,
300 statements_run: Cell<usize>,
303 owners: RefCell<HashMap<(Target, String), Option<Value>>>,
306 now_ms: i64,
311}
312
313struct RowsRoot {
318 rows: RefCell<Option<Vec<Value>>>,
319 once: bool,
320}
321
322fn sql_value(v: ValueRef<'_>) -> Value {
323 match v {
324 ValueRef::Null => Value::Null,
325 ValueRef::Integer(i) => Value::Number(i as f64),
326 ValueRef::Real(f) => Value::Number(f),
327 ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
328 ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
329 }
330}
331
332pub(crate) fn to_sql(v: &Value) -> SqlValue {
337 match v {
338 Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
339 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
340 Value::Number(n) => SqlValue::Real(*n),
341 Value::Str(s) => SqlValue::Text(s.clone()),
342 Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
343 }
344}
345
346fn js_string(v: &Value) -> String {
348 v.to_string()
349}
350
351fn arg_or_empty(args: &[Value], i: usize) -> String {
353 match args.get(i) {
354 None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
355 Some(v) => js_string(v),
356 }
357}
358
359fn parse_json(v: &Value) -> Value {
361 match v {
362 Value::Str(s) => {
363 serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
364 }
365 Value::Null | Value::Undefined => Value::Undefined,
366 other => other.clone(),
367 }
368}
369
370fn sql_err(e: rusqlite::Error) -> OqxError {
373 OqxError::eval(format!("sqlite: {e}"))
374}
375
376pub fn target_of(row: &Value) -> Option<Target> {
378 row.as_object()
379 .and_then(|o| o.get(TAG_KEY))
380 .and_then(Value::as_str)
381 .and_then(Target::parse)
382}
383
384fn is_lazy_owner(row: &Value) -> bool {
386 row.as_object().is_some_and(|o| o.contains_key(LAZY_KEY))
387}
388
389#[must_use]
391pub fn scan_of(v: &Value) -> Option<Target> {
392 v.as_object()
393 .and_then(|o| o.get(SCAN_KEY))
394 .and_then(Value::as_str)
395 .and_then(Target::parse)
396}
397
398fn scan_marker(t: Target) -> Value {
399 let mut o = Object::with_capacity(1);
400 o.insert(SCAN_KEY, Value::Str(t.as_str().to_owned()));
401 Value::Object(o)
402}
403
404pub const ALL_TARGETS: [Target; 5] = [
407 Target::Docs,
408 Target::Blocks,
409 Target::Nodes,
410 Target::Links,
411 Target::Revisions,
412];
413
414fn is_root_object(row: &Value) -> bool {
419 row.as_object().is_some_and(|o| {
420 o.len() == ALL_TARGETS.len()
421 && ALL_TARGETS
422 .iter()
423 .all(|t| o.get(t.as_str()).is_some_and(|v| scan_of(v) == Some(*t)))
424 })
425}
426
427fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
428 row.as_object()
429 .and_then(|o| o.get(key))
430 .unwrap_or(&Value::Undefined)
431}
432
433fn col_str(row: &Value, key: &str) -> String {
434 match col(row, key) {
435 Value::Undefined | Value::Null => String::new(),
436 v => js_string(v),
437 }
438}
439
440#[must_use]
442pub fn strip_tags(v: Value) -> Value {
443 match v {
444 Value::Object(o) => Value::Object(
445 o.into_iter()
446 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
447 .map(|(k, x)| (k, strip_tags(x)))
448 .collect(),
449 ),
450 Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
451 other => other,
452 }
453}
454
455#[must_use]
464pub fn render_row_values(v: Value) -> Value {
465 match v {
466 Value::Object(o) => {
467 let row = Value::Object(o);
468 if let Some(t) = target_of(&row) {
469 let (id_col, path_col) = match t {
470 Target::Docs => ("doc_id", "path"),
471 Target::Blocks => ("block_id", "__path"),
472 Target::Nodes => ("node_id", "__path"),
473 Target::Links => ("edge_id", "__path"),
474 Target::Revisions => ("rev_id", "path"),
477 };
478 let mut out = Object::with_capacity(2);
479 out.insert(crate::query::HIT_ID, Value::Str(col_str(&row, id_col)));
480 let path = match col(&row, path_col) {
481 Value::Undefined | Value::Null => String::new(),
482 v => reference_path(&js_string(v)),
483 };
484 out.insert(crate::query::HIT_PATH, Value::Str(path));
485 return Value::Object(out);
486 }
487 let Value::Object(o) = row else {
488 unreachable!()
489 };
490 Value::Object(
491 o.into_iter()
492 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
493 .map(|(k, x)| (k, render_row_values(x)))
494 .collect(),
495 )
496 }
497 Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
498 other => other,
499 }
500}
501
502impl<'a> StoreContext<'a> {
503 #[must_use]
506 pub fn new(
507 conn: &'a Connection,
508 repo_id: &str,
509 semantic: HashMap<String, SemanticVec>,
510 ) -> Self {
511 Self {
512 conn,
513 repo_id: repo_id.to_owned(),
514 semantic,
515 root_failure: RefCell::new(None),
516 rows_root: None,
517 regexes: RefCell::new(HashMap::new()),
518 scans: RefCell::new(HashMap::new()),
519 scans_run: Cell::new(0),
520 statements_run: Cell::new(0),
521 owners: RefCell::new(HashMap::new()),
522 now_ms: dates::wall_clock_ms(),
523 }
524 }
525
526 #[must_use]
530 pub fn with_now_ms(mut self, now_ms: i64) -> Self {
531 self.now_ms = now_ms;
532 self
533 }
534
535 #[must_use]
537 pub fn now_ms(&self) -> i64 {
538 self.now_ms
539 }
540
541 #[must_use]
544 pub fn statements_run(&self) -> usize {
545 self.statements_run.get()
546 }
547
548 #[must_use]
550 pub fn repo_id(&self) -> &str {
551 &self.repo_id
552 }
553
554 #[must_use]
558 pub fn scans_run(&self) -> usize {
559 self.scans_run.get()
560 }
561
562 pub(crate) fn scan_rows(&self, t: Target) -> oqx::Result<Rc<Vec<Value>>> {
564 if let Some(rows) = self.scans.borrow().get(&t) {
565 return Ok(Rc::clone(rows));
566 }
567 self.scans_run.set(self.scans_run.get() + 1);
568 let rows = match self.root_scan(t)? {
569 Value::Array(rows) => Rc::new(rows),
570 _ => Rc::new(Vec::new()),
571 };
572 self.scans.borrow_mut().insert(t, Rc::clone(&rows));
573 Ok(rows)
574 }
575
576 fn refs(&self, x: &Value) -> oqx::Result<Value> {
584 let items: Vec<&Value> = match x {
585 Value::Undefined | Value::Null => Vec::new(),
586 Value::Array(a) => a.iter().collect(),
587 other => vec![other],
588 };
589 let mut out = Vec::new();
590 for item in items {
591 if let Value::Str(s) = item
592 && let Some(row) = self.ref_doc(s)?
593 {
594 out.push(row);
595 }
596 }
597 Ok(Value::Array(out))
598 }
599
600 fn ref_doc(&self, r: &str) -> oqx::Result<Option<Value>> {
606 let t = Target::Docs;
607 let lookup = |column: &str, value: &str| -> oqx::Result<Option<Value>> {
608 let sql = format!(
609 "SELECT {} FROM {} WHERE {} AND d.{column} = ? ORDER BY {}",
610 planner::columns(t),
611 planner::from_clause(t),
612 planner::guards(t),
613 planner::order_clause(t)
614 );
615 let params = [
616 SqlValue::Text(self.repo_id.clone()),
617 SqlValue::Text(value.to_owned()),
618 ];
619 Ok(self.one(&sql, ¶ms)?.map(|row| tag_row(row, t)))
620 };
621 if let Some(row) = lookup("path", storage_path(r))? {
622 return Ok(Some(row));
623 }
624 if r.starts_with("d_") {
625 return lookup("doc_id", r);
626 }
627 Ok(None)
628 }
629
630 pub(crate) fn probe_rows(
632 &self,
633 t: Target,
634 sql: &str,
635 params: &[SqlValue],
636 ) -> oqx::Result<Vec<Value>> {
637 Ok(tag_rows(self.all(sql, params)?, t))
638 }
639
640 fn expand_scan(&self, t: Target) -> Vec<Value> {
643 match self.scan_rows(t) {
644 Ok(rows) => rows.as_ref().clone(),
645 Err(e) => {
646 let mut slot = self.root_failure.borrow_mut();
647 if slot.is_none() {
648 *slot = Some(e);
649 }
650 Vec::new()
651 }
652 }
653 }
654
655 #[must_use]
660 pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
661 self.rows_root = Some(RowsRoot {
662 rows: RefCell::new(Some(rows)),
663 once: false,
664 });
665 self
666 }
667
668 #[must_use]
676 pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
677 self.rows_root = Some(RowsRoot {
678 rows: RefCell::new(Some(rows)),
679 once: true,
680 });
681 self
682 }
683
684 pub fn take_root_failure(&self) -> Option<OqxError> {
689 self.root_failure.borrow_mut().take()
690 }
691
692 fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
695 self.statements_run.set(self.statements_run.get() + 1);
696 fetch_rows(self.conn, sql, params).map_err(sql_err)
697 }
698
699 fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
700 Ok(self.all(sql, params)?.into_iter().next())
701 }
702
703 fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
704 Ok(self
705 .one(sql, params)?
706 .and_then(|o| o.values().next().cloned())
707 .unwrap_or(Value::Undefined))
708 }
709
710 fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
711 self.statements_run.set(self.statements_run.get() + 1);
712 let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
713 stmt.exists(params_from_iter(params.iter()))
714 .map_err(sql_err)
715 }
716
717 fn tag_all(rows: Vec<Object>, t: Target) -> Value {
718 Value::Array(tag_rows(rows, t))
719 }
720
721 fn tag(row: Object, t: Target) -> Value {
722 tag_row(row, t)
723 }
724
725 fn root_row(&self) -> Value {
733 let mut o = Object::with_capacity(ALL_TARGETS.len());
734 for t in ALL_TARGETS {
735 o.insert(t.as_str(), scan_marker(t));
736 }
737 Value::Object(o)
738 }
739
740 fn root_scan(&self, t: Target) -> oqx::Result<Value> {
753 let repo = [SqlValue::Text(self.repo_id.clone())];
754 let sql = match t {
755 Target::Docs => {
756 "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
757 }
758 Target::Blocks => {
759 "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
760 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
761 ORDER BY d.path, b.block_id"
762 }
763 Target::Nodes => {
764 "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
765 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
766 }
767 Target::Links => {
768 "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
769 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
770 ORDER BY d.path, e.edge_id"
771 }
772 Target::Revisions => {
775 let sql = format!(
776 "SELECT {REVISION_COLS} FROM {REVISION_FROM} WHERE d.repo_id = ?1 ORDER BY r.path, r.rev_id"
777 );
778 return Ok(Self::tag_all(self.all(&sql, &repo)?, t));
779 }
780 };
781 Ok(Self::tag_all(self.all(sql, &repo)?, t))
782 }
783
784 fn doc_revisions(&self, doc_id: &str) -> oqx::Result<Value> {
787 let sql = format!(
788 "SELECT {REVISION_COLS} FROM {REVISION_FROM} WHERE r.doc_id = ?1 ORDER BY r.seq"
789 );
790 Ok(Self::tag_all(
791 self.all(&sql, &[SqlValue::Text(doc_id.to_owned())])?,
792 Target::Revisions,
793 ))
794 }
795
796 fn decode_prop(r: &Object) -> Value {
801 let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
802 match get("type").as_str().unwrap_or("") {
803 "number" => get("val_num"),
804 "bool" => Value::Bool(get("val_bool").truthy()),
805 "null" => Value::Null,
806 "json" => parse_json(&get("val_json")),
807 _ => get("val_text"),
808 }
809 }
810
811 fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
814 let rows = match source {
815 Some(s) => self.all(
816 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
817 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
818 )?,
819 None => self.all(
820 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
821 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
822 )?,
823 };
824 if rows.is_empty() {
825 return self.doc_prop_object(doc_id, key, source);
826 }
827 if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
828 return Ok(Self::decode_prop(&rows[0]));
829 }
830 Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
831 }
832
833 fn doc_prop_object(
836 &self,
837 doc_id: &str,
838 prefix: &str,
839 source: Option<&str>,
840 ) -> oqx::Result<Value> {
841 let like = SqlValue::Text(format!("{prefix}.%"));
842 let rows = match source {
843 Some(s) => self.all(
844 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
845 &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
846 )?,
847 None => self.all(
848 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
849 &[SqlValue::Text(doc_id.to_owned()), like],
850 )?,
851 };
852 if rows.is_empty() {
853 return Ok(Value::Undefined);
854 }
855 let mut out = Object::new();
856 for r in &rows {
857 let key = r.get("key").and_then(Value::as_str).unwrap_or("");
858 let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
859 .split('.')
860 .collect();
861 set_nested(&mut out, &rest, Self::decode_prop(r));
862 }
863 Ok(Value::Object(out))
864 }
865
866 fn doc_prop_bag(&self, doc_id: &str, source: Option<&str>) -> oqx::Result<Value> {
871 let keys = match source {
872 Some(s) => self.all(
873 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
874 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(s.to_owned())],
875 )?,
876 None => self.all(
877 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source IN ('frontmatter', 'inline') AND deleted_commit IS NULL ORDER BY key",
878 &[SqlValue::Text(doc_id.to_owned())],
879 )?,
880 };
881 let mut out = Object::new();
882 for k in keys {
883 let key = k.get("key").and_then(Value::as_str).unwrap_or("");
884 let top = key.split('.').next().unwrap_or("");
885 if !out.contains_key(top) {
886 let v = self.doc_prop(doc_id, top, source)?;
887 out.insert(top, v);
888 }
889 }
890 Ok(Value::Object(out))
891 }
892
893 fn doc_tags(&self, doc_id: &str) -> oqx::Result<Value> {
899 fn as_list(v: Value) -> Vec<Value> {
900 match v {
901 Value::Undefined | Value::Null => Vec::new(),
902 Value::Array(a) => a,
903 other => vec![other],
904 }
905 }
906 let authored = as_list(self.doc_prop(doc_id, "tags", None)?);
907 let hashtags = as_list(self.doc_prop(doc_id, "$tags", Some("computed"))?);
908 if authored.is_empty() && hashtags.is_empty() {
909 return Ok(Value::Null);
910 }
911 let mut seen: Vec<String> = Vec::new();
912 let mut out = Vec::new();
913 for v in authored.into_iter().chain(hashtags) {
914 let k = v.to_canonical_json().to_string();
915 if seen.contains(&k) {
916 continue;
917 }
918 seen.push(k);
919 out.push(v);
920 }
921 Ok(Value::Array(out))
922 }
923
924 fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
929 let ordinal = col(block, "ordinal").clone();
930 if col(block, "parent_block").is_absent() {
931 return Ok(ordinal);
932 }
933 let ap = col_str(block, "ancestor_path");
934 let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
935 return Ok(ordinal);
936 };
937 let r = self.scalar(
938 "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
939 &[
940 SqlValue::Text(col_str(block, "doc_id")),
941 SqlValue::Text(first.to_owned()),
942 ],
943 )?;
944 Ok(if r.is_absent() { ordinal } else { r })
945 }
946
947 fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
951 let rows = self.all(
952 "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",
953 &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
954 )?;
955 let ids: Vec<String> = rows
956 .iter()
957 .map(|r| {
958 r.get("block_id")
959 .and_then(Value::as_str)
960 .unwrap_or("")
961 .to_owned()
962 })
963 .collect();
964 let parent_index: Vec<Option<usize>> = rows
965 .iter()
966 .map(|r| {
967 r.get("parent_block")
968 .and_then(Value::as_str)
969 .and_then(|p| ids.iter().position(|id| id == p))
970 })
971 .collect();
972 let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
973 let mut roots = Vec::new();
974 for (i, p) in parent_index.iter().enumerate() {
975 match p {
976 Some(p) => children[*p].push(i),
977 None => roots.push(i),
978 }
979 }
980 fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
981 order.push(i);
982 for &c in &children[i] {
983 walk(c, children, order);
984 }
985 }
986 let mut order = Vec::with_capacity(rows.len());
987 for r in roots {
988 walk(r, &children, &mut order);
989 }
990 let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
991 Ok(order
992 .into_iter()
993 .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
994 .collect())
995 }
996
997 fn jattr(row: &Value, k: &str) -> Value {
998 match parse_json(col(row, "attrs")) {
999 Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
1000 _ => Value::Undefined,
1001 }
1002 }
1003
1004 fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
1006 let path = || SqlValue::Text(col_str(row, "__path"));
1007 let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
1008 let doc_path = || SqlValue::Text(col_str(row, "path"));
1009 let block_id = || SqlValue::Text(col_str(row, "block_id"));
1010 Ok(Some(match (t, key) {
1011 (Target::Docs, "nodes") => {
1012 let rows = self.all(
1015 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
1016 &[doc_path(), doc_id()],
1017 )?;
1018 let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
1019 let rank: HashMap<String, usize> = blocks
1020 .iter()
1021 .enumerate()
1022 .map(|(i, b)| (col_str(b, "block_id"), i))
1023 .collect();
1024 let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
1025 .into_iter()
1026 .map(|r| {
1027 let block = r.get("block_id").and_then(Value::as_str);
1028 let (has_block, rk) = match block {
1029 None => (0, 0),
1030 Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
1031 };
1032 let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
1033 let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
1034 ((has_block, rk, span, id), r)
1035 })
1036 .collect();
1037 keyed.sort_by(|a, b| {
1038 a.0.0
1039 .cmp(&b.0.0)
1040 .then(a.0.1.cmp(&b.0.1))
1041 .then(a.0.2.total_cmp(&b.0.2))
1042 .then(a.0.3.cmp(&b.0.3))
1043 });
1044 Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
1045 }
1046 (Target::Docs, "blocks") => {
1047 Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
1048 }
1049 (Target::Docs, "revisions") => self.doc_revisions(&col_str(row, "doc_id"))?,
1051 (Target::Docs, "out_links") => Self::tag_all(
1056 self.all(
1057 "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",
1058 &[doc_path(), doc_id()],
1059 )?,
1060 Target::Links,
1061 ),
1062 (Target::Docs, "in_links") => Self::tag_all(
1063 self.all(
1064 "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
1065 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
1066 &[doc_id()],
1067 )?,
1068 Target::Links,
1069 ),
1070 (Target::Blocks, "children") => Self::tag_all(
1071 self.all(
1072 "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",
1073 &[path(), block_id()],
1074 )?,
1075 Target::Blocks,
1076 ),
1077 (Target::Blocks, "nodes") => Self::tag_all(
1078 self.all(
1079 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
1080 &[path(), block_id()],
1081 )?,
1082 Target::Nodes,
1083 ),
1084 (Target::Blocks, "out_links") => Self::tag_all(
1085 self.all(
1086 "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",
1087 &[path(), block_id()],
1088 )?,
1089 Target::Links,
1090 ),
1091 (Target::Blocks, "section") => {
1092 let top = to_sql(&self.top_ordinal(row)?);
1093 Self::tag_all(
1094 self.all(
1095 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
1096 AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
1097 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1098 &[path(), doc_id(), top.clone(), top],
1099 )?,
1100 Target::Nodes,
1101 )
1102 }
1103 (Target::Nodes, "blocks") => {
1104 let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
1105 if f.is_absent() || l.is_absent() {
1106 return Ok(Some(Value::Array(Vec::new())));
1107 }
1108 let (f, l) = (
1109 f.as_f64().unwrap_or(f64::NAN),
1110 l.as_f64().unwrap_or(f64::NAN),
1111 );
1112 let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
1113 let mut kept: Vec<Value> = Vec::new();
1114 for b in rows {
1115 let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
1116 if t >= f && t <= l {
1117 kept.push(b);
1118 }
1119 }
1120 Value::Array(kept)
1121 }
1122 (Target::Nodes, "subsections") => {
1123 let (f, l, lvl) = (
1124 Self::jattr(row, "first_ordinal"),
1125 Self::jattr(row, "last_ordinal"),
1126 Self::jattr(row, "level"),
1127 );
1128 if f.is_absent() {
1129 return Ok(Some(Value::Array(Vec::new())));
1130 }
1131 Self::tag_all(
1132 self.all(
1133 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
1134 AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
1135 AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1136 &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1137 )?,
1138 Target::Nodes,
1139 )
1140 }
1141 (Target::Nodes, "children") => {
1142 let (f, l, lvl) = (
1143 Self::jattr(row, "first_ordinal"),
1144 Self::jattr(row, "last_ordinal"),
1145 Self::jattr(row, "level"),
1146 );
1147 if f.is_absent() {
1148 return Ok(Some(Value::Array(Vec::new())));
1149 }
1150 Self::tag_all(
1151 self.all(
1152 "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
1153 AND json_extract(i.attrs,'$.level') > ?3
1154 AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
1155 AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
1156 AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
1157 AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
1158 AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
1159 ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
1160 &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1161 )?,
1162 Target::Nodes,
1163 )
1164 }
1165 _ => return Ok(None),
1166 }))
1167 }
1168
1169 fn lazy_owner(known: &[(&str, Value)], t: Target) -> Value {
1182 let mut o = Object::with_capacity(known.len() + 2);
1183 for (k, v) in known {
1184 o.insert(*k, v.clone());
1185 }
1186 o.insert(LAZY_KEY, Value::Bool(true));
1187 tag_row(o, t)
1188 }
1189
1190 fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
1191 let id = match col(row, "doc_id") {
1192 Value::Undefined | Value::Null => col(row, "src_doc").clone(),
1193 v => v.clone(),
1194 };
1195 if id.is_absent() {
1196 return Ok(absent_single());
1197 }
1198 match col(row, "__path") {
1199 Value::Str(path) => Ok(Self::lazy_owner(
1200 &[("doc_id", id), ("path", Value::Str(path.clone()))],
1201 Target::Docs,
1202 )),
1203 _ => Ok(self
1204 .load_owner(Target::Docs, &js_string(&id))?
1205 .unwrap_or_else(absent_single)),
1206 }
1207 }
1208
1209 fn destination_doc(&self, row: &Value) -> oqx::Result<Value> {
1216 if col(row, "dst_kind") != &Value::Str("document".to_owned()) {
1217 return Ok(absent_single());
1218 }
1219 Ok(self
1220 .load_owner(Target::Docs, &col_str(row, "dst_node"))?
1221 .unwrap_or_else(absent_single))
1222 }
1223
1224 fn revision_doc(&self, row: &Value) -> oqx::Result<Value> {
1231 Ok(self
1232 .one(
1233 "SELECT * FROM docs WHERE doc_id = ?1 AND deleted_commit IS NULL",
1234 &[SqlValue::Text(col_str(row, "doc_id"))],
1235 )?
1236 .map_or_else(absent_single, |o| Self::tag(o, Target::Docs)))
1237 }
1238
1239 fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
1240 let id = col(row, "block_id");
1241 if !id.truthy() {
1242 return Ok(absent_single());
1243 }
1244 match col(row, "__path") {
1245 Value::Str(path) => Ok(Self::lazy_owner(
1246 &[
1247 ("block_id", id.clone()),
1248 ("doc_id", col(row, "doc_id").clone()),
1249 ("__path", Value::Str(path.clone())),
1250 ],
1251 Target::Blocks,
1252 )),
1253 _ => Ok(self
1254 .load_owner(Target::Blocks, &js_string(id))?
1255 .unwrap_or_else(absent_single)),
1256 }
1257 }
1258
1259 fn link_block(&self, row: &Value) -> oqx::Result<Value> {
1265 let id = col(row, "src_block");
1266 if !id.truthy() {
1267 return Ok(absent_single());
1268 }
1269 match col(row, "__path") {
1270 Value::Str(path) => Ok(Self::lazy_owner(
1271 &[
1272 ("block_id", id.clone()),
1273 ("doc_id", col(row, "src_doc").clone()),
1274 ("__path", Value::Str(path.clone())),
1275 ],
1276 Target::Blocks,
1277 )),
1278 _ => Ok(self
1279 .load_owner(Target::Blocks, &js_string(id))?
1280 .unwrap_or_else(absent_single)),
1281 }
1282 }
1283
1284 fn load_owner(&self, t: Target, id: &str) -> oqx::Result<Option<Value>> {
1287 let key = (t, id.to_owned());
1288 if let Some(v) = self.owners.borrow().get(&key) {
1289 return Ok(v.clone());
1290 }
1291 let loaded = match t {
1292 Target::Docs => self
1293 .one(
1294 "SELECT * FROM docs WHERE doc_id = ?1",
1295 &[SqlValue::Text(id.to_owned())],
1296 )?
1297 .map(|o| Self::tag(o, Target::Docs)),
1298 _ => self
1299 .one(
1300 "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
1301 &[SqlValue::Text(id.to_owned())],
1302 )?
1303 .map(|o| Self::tag(o, Target::Blocks)),
1304 };
1305 self.owners.borrow_mut().insert(key, loaded.clone());
1306 Ok(loaded)
1307 }
1308
1309 fn lazy_needs_load(t: Target, key: &str) -> bool {
1319 match t {
1320 Target::Docs => matches!(key, "format" | "hash" | "updated_at"),
1321 Target::Blocks => {
1322 !matches!(
1323 key,
1324 "id" | "path"
1325 | "updated_at"
1326 | "$self"
1327 | "doc_id"
1328 | "block"
1329 | "doc"
1330 | "children"
1331 | "nodes"
1332 | "out_links"
1333 ) && Target::parse(key).is_none()
1334 }
1335 Target::Nodes | Target::Links | Target::Revisions => true,
1337 }
1338 }
1339
1340 fn resolve_lazy(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
1344 if !is_lazy_owner(row) || !Self::lazy_needs_load(t, key) {
1345 return Ok(None);
1346 }
1347 let id_col = if t == Target::Docs {
1348 "doc_id"
1349 } else {
1350 "block_id"
1351 };
1352 self.load_owner(t, &col_str(row, id_col))
1353 }
1354
1355 fn null_if_absent(v: Value) -> Value {
1358 if v.is_absent() { Value::Null } else { v }
1359 }
1360
1361 fn attribute(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
1370 let c = |k: &str| col(row, k).clone();
1371 let rooted = |k: &str| Value::Str(reference_path(&col_str(row, k)));
1372 Ok(match (t, name) {
1373 (Target::Docs, "id") => c("doc_id"),
1374 (Target::Docs, "path") => rooted("path"),
1375 (Target::Docs, "hash") => Self::null_if_absent(c("file_hash")),
1376 (Target::Docs, "updated_at") => Self::null_if_absent(self.scalar(
1377 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
1378 &[to_sql(&c("current_rev"))],
1379 )?),
1380 (Target::Docs, "created_at") => Self::null_if_absent(self.scalar(
1387 "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",
1388 &[to_sql(&c("doc_id"))],
1389 )?),
1390 (Target::Docs, "raw") => {
1392 match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
1393 Ok(Some(s)) => Value::Str(s),
1394 Ok(None) => Value::Null,
1395 Err(e) => return Err(OqxError::eval(e.to_string())),
1396 }
1397 }
1398 (Target::Docs, "title") => {
1403 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
1404 }
1405 (Target::Docs, "tags") => self.doc_tags(&col_str(row, "doc_id"))?,
1408 (Target::Docs, "format") => c("format"),
1409 (Target::Blocks, "id") => c("block_id"),
1410 (Target::Blocks, "path") => rooted("__path"),
1411 (Target::Blocks, "raw") => match c("raw_hash") {
1415 Value::Str(hex) => {
1416 let hash = omgbase_store::tree::from_hex(&hex)
1417 .map_err(|e| OqxError::eval(e.to_string()))?;
1418 let bytes: Option<Vec<u8>> = self
1419 .conn
1420 .query_row(
1421 "SELECT bytes FROM blobs WHERE hash = ?1",
1422 rusqlite::params![hash],
1423 |r| r.get(0),
1424 )
1425 .optional()
1426 .map_err(sql_err)?;
1427 self.statements_run.set(self.statements_run.get() + 1);
1428 bytes.map_or(Value::Null, |b| {
1429 Value::Str(String::from_utf8_lossy(&b).into_owned())
1430 })
1431 }
1432 _ => Value::Null,
1433 },
1434 (Target::Blocks, "hash") => Self::null_if_absent(c("raw_hash")),
1435 (Target::Blocks, "updated_at") => Self::null_if_absent(self.scalar(
1436 "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
1437 &[to_sql(&c("block_id"))],
1438 )?),
1439 (Target::Blocks, "created_at") => Self::null_if_absent(self.scalar(
1447 "SELECT MIN(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
1448 &[to_sql(&c("block_id"))],
1449 )?),
1450 (Target::Blocks, "doc_id" | "type" | "text" | "ordinal" | "depth") => c(name),
1453 (Target::Blocks | Target::Nodes, "attrs") => parse_json(&c("attrs")),
1454 (Target::Nodes, "id") => c("node_id"),
1455 (Target::Nodes, "path") => rooted("__path"),
1456 (Target::Nodes, "doc_id" | "kind" | "name" | "value") => c(name),
1457 (Target::Nodes, "block_id") => Self::null_if_absent(c("block_id")),
1458 (Target::Links, "id") => c("edge_id"),
1461 (Target::Links, "path") => rooted("__path"),
1462 (Target::Links, "doc_id") => c("src_doc"),
1463 (Target::Links, "block_id") => Self::null_if_absent(c("src_block")),
1464 (Target::Links, "via_id") => Self::null_if_absent(c("via_node")),
1465 (Target::Links, "dst_id") => c("dst_node"),
1466 (Target::Links, "dst_path") => match self.scalar(
1467 "SELECT path FROM docs WHERE doc_id = ?1",
1468 &[to_sql(&c("dst_node"))],
1469 )? {
1470 Value::Undefined | Value::Null => Value::Null,
1471 p => Value::Str(reference_path(&js_string(&p))),
1472 },
1473 (Target::Links, "uri") => Self::null_if_absent(self.scalar(
1474 "SELECT uri FROM external_nodes WHERE node_id = ?1",
1475 &[to_sql(&c("dst_node"))],
1476 )?),
1477 (Target::Links, "field") => Self::null_if_absent(c("src_field")),
1478 (Target::Links, "predicate" | "provenance" | "dst_kind" | "anchor") => c(name),
1479 (Target::Revisions, "id" | "rev") => c("rev_id"),
1483 (Target::Revisions, "path") => rooted("path"),
1484 (Target::Revisions, "seq" | "doc_id" | "ts" | "origin") => c(name),
1485 (Target::Revisions, "commit") => c("commit_id"),
1486 (Target::Revisions, "actor" | "reason") => Self::null_if_absent(c(name)),
1487 (Target::Revisions, "hash") => c("rendered_hash"),
1488 (Target::Revisions, "is_current" | "archived") => Value::Bool(matches!(
1489 c(name),
1490 Value::Number(n) if n == 1.0
1491 )),
1492 _ => Value::Undefined,
1493 })
1494 }
1495
1496 fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
1499 Some(Err(OqxError::eval(msg)))
1500 }
1501
1502 fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
1503 (t != want).then(|| {
1504 Err(OqxError::eval(format!(
1505 "{name}() is only available on the {} target",
1506 want.as_str()
1507 )))
1508 })
1509 }
1510
1511 fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
1512 r.map(Value::Bool)
1513 }
1514
1515 fn row_method(
1516 &self,
1517 name: &str,
1518 row: &Value,
1519 t: Target,
1520 args: &[Value],
1521 ) -> Option<oqx::Result<Value>> {
1522 let c = |k: &str| col(row, k).clone();
1523 match name {
1524 "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
1525 "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
1526 "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1527 Some(Self::sql_result(self.exists(
1528 "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
1529 &[to_sql(&c("block_id"))],
1530 )))
1531 }),
1532 "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1533 Some(self.scalar(
1534 "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
1535 &[to_sql(&c("block_id"))],
1536 ))
1537 }),
1538 "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1539 Some(
1540 self.scalar(
1541 "SELECT type FROM blocks WHERE block_id = ?1",
1542 &[to_sql(&c("parent_block"))],
1543 )
1544 .map(Self::null_if_absent),
1545 )
1546 }),
1547 "has_edge" => {
1548 let pred = js_string(args.first().unwrap_or(&Value::Undefined));
1549 let (src_col, src_val) = if t == Target::Blocks {
1550 ("src_block", c("block_id"))
1551 } else {
1552 ("src_doc", c("doc_id"))
1553 };
1554 let r = if args.len() >= 2 {
1555 self.exists(
1556 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
1557 &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
1558 )
1559 } else {
1560 self.exists(
1561 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
1562 &[to_sql(&src_val), SqlValue::Text(pred)],
1563 )
1564 };
1565 Some(Self::sql_result(r))
1566 }
1567 "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1568 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1569 let ap = col_str(row, "ancestor_path");
1570 Some(Ok(Value::Bool(
1571 ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
1572 )))
1573 }),
1574 "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1575 let text = js_string(args.first().unwrap_or(&Value::Undefined));
1576 let top = match self.top_ordinal(row) {
1577 Ok(v) => to_sql(&v),
1578 Err(e) => return Some(Err(e)),
1579 };
1580 Some(Self::sql_result(self.exists(
1581 "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
1582 WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
1583 &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
1584 )))
1585 }),
1586 "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1589 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1590 if target.starts_with("d_") {
1591 return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
1592 }
1593 let target = storage_path(&target).to_owned();
1594 if target.contains('*') {
1595 let like = glob_to_like(&target, false);
1596 return Some(Self::sql_result(self.exists(
1597 "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
1598 &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
1599 )));
1600 }
1601 Some(Ok(Value::Bool(col_str(row, "__path") == target)))
1602 }),
1603 "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1604 let kind = js_string(args.first().unwrap_or(&Value::Undefined));
1605 let ap: Vec<String> = col_str(row, "ancestor_path")
1606 .split('/')
1607 .filter(|s| !s.is_empty())
1608 .map(str::to_owned)
1609 .collect();
1610 if ap.is_empty() {
1611 return Some(Ok(Value::Bool(false)));
1612 }
1613 let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
1614 let placeholders = placeholders.join(",");
1615 let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
1616 let n = params.len();
1617 params.push(SqlValue::Text(kind));
1618 let r = match args.get(1) {
1619 Some(v) if !v.is_absent() => {
1620 let nm = js_string(v);
1621 params.push(SqlValue::Text(nm.clone()));
1622 params.push(SqlValue::Text(nm));
1623 self.exists(
1624 &format!(
1625 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
1626 n + 1,
1627 n + 2,
1628 n + 3
1629 ),
1630 ¶ms,
1631 )
1632 }
1633 _ => self.exists(
1634 &format!(
1635 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
1636 n + 1
1637 ),
1638 ¶ms,
1639 ),
1640 };
1641 Some(Self::sql_result(r))
1642 }),
1643 "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1644 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
1645 }),
1646 "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1647 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
1648 }),
1649 _ => None,
1650 }
1651 }
1652
1653 fn key_path(row: &Value, path: &str, kind: &str) -> Value {
1654 let key = if kind == "json" {
1655 let mut p = path;
1656 p = p.strip_prefix('#').unwrap_or(p);
1657 p = p.strip_prefix('/').unwrap_or(p);
1658 p.split('/').collect::<Vec<_>>().join(".")
1659 } else {
1660 path.to_owned()
1661 };
1662 let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
1663 if !col_str(row, "type").starts_with(&format!("{kind}:")) {
1664 return Value::Bool(false);
1665 }
1666 let k = Self::jattr(row, "key");
1667 Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
1668 }
1669
1670 fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
1671 if matches!(t, Target::Links | Target::Revisions) {
1673 return Err(OqxError::eval(format!(
1674 "text(...) is not available on the {} target",
1675 t.as_str()
1676 )));
1677 }
1678 let m = sanitize_fts_query(terms);
1679 if m.is_empty() {
1680 return Ok(Value::Bool(false));
1681 }
1682 let r = match t {
1683 Target::Docs => self.exists(
1684 "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",
1685 &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1686 ),
1687 Target::Nodes => self.exists(
1688 "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1689 &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1690 ),
1691 _ => self.exists(
1692 "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1693 &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1694 ),
1695 };
1696 Self::sql_result(r)
1697 }
1698
1699 fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1709 let flags = match args.get(1) {
1710 None | Some(Value::Undefined) | Some(Value::Null) => None,
1711 Some(Value::Str(s)) => Some(s.clone()),
1712 Some(_) => return None,
1713 };
1714 let Some(subject) = string_form(recv) else {
1715 return Some(Ok(Value::Bool(false)));
1716 };
1717 let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1718 let key = (pattern, flags);
1719 let mut memo = self.regexes.borrow_mut();
1720 if !memo.contains_key(&key) {
1721 let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1722 match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1723 Ok(re) => {
1724 memo.insert(key.clone(), re);
1725 }
1726 Err(_) => return None,
1727 }
1728 }
1729 Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1730 }
1731
1732 fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1733 if matches!(t, Target::Nodes | Target::Links | Target::Revisions) {
1734 return Err(OqxError::eval(
1735 "semantic(...) is available on the docs and blocks targets",
1736 ));
1737 }
1738 let Some(resolved) = self.semantic.get(phrase) else {
1739 return Err(OqxError::eval(format!(
1740 "semantic({}) needs an embedding provider; none is configured for this query",
1741 serde_json::Value::String(phrase.to_owned())
1742 )));
1743 };
1744 let vec: oqx::Result<Option<Vec<u8>>> = match t {
1745 Target::Docs => self
1746 .conn
1747 .query_row(
1748 "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1749 rusqlite::params![col_str(row, "doc_id"), resolved.model],
1750 |r| r.get(0),
1751 )
1752 .optional()
1753 .map_err(sql_err),
1754 _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1757 .map_err(|e| OqxError::eval(e.to_string())),
1758 };
1759 match vec? {
1760 Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1761 None => Ok(Value::Null),
1762 }
1763 }
1764}
1765
1766pub(crate) fn fetch_rows(
1769 conn: &Connection,
1770 sql: &str,
1771 params: &[SqlValue],
1772) -> rusqlite::Result<Vec<Object>> {
1773 let mut stmt = conn.prepare_cached(sql)?;
1774 let names: Vec<String> = stmt
1775 .column_names()
1776 .iter()
1777 .map(|s| (*s).to_owned())
1778 .collect();
1779 let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1780 let mut o = Object::with_capacity(names.len() + 1);
1783 for (i, name) in names.iter().enumerate() {
1784 o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1785 }
1786 Ok(o)
1787 })?;
1788 rows.collect()
1789}
1790
1791pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1794 row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1795 Value::Object(row)
1796}
1797
1798pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1800 rows.into_iter().map(|r| tag_row(r, t)).collect()
1801}
1802
1803fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1806 let Some((first, rest)) = path.split_first() else {
1807 return;
1808 };
1809 if rest.is_empty() {
1810 out.insert(*first, leaf);
1811 return;
1812 }
1813 let mut child = match out.get(first) {
1814 Some(Value::Object(o)) => o.clone(),
1815 _ => Object::new(),
1816 };
1817 set_nested(&mut child, rest, leaf);
1818 out.insert(*first, Value::Object(child));
1819}
1820
1821#[must_use]
1824pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1825 let mut out = String::with_capacity(glob.len() + 4);
1826 for ch in glob.chars() {
1827 match ch {
1828 '%' | '_' => {
1829 out.push('\\');
1830 out.push(ch);
1831 }
1832 '\\' if escape_backslash => out.push_str("\\\\"),
1833 '*' => out.push('%'),
1834 c => out.push(c),
1835 }
1836 }
1837 out
1838}
1839
1840impl DataContext for StoreContext<'_> {
1841 fn root(&self, name: &str) -> Value {
1842 if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1843 let mut slot = rr.rows.borrow_mut();
1844 let rows = if rr.once { slot.take() } else { slot.clone() };
1845 return Value::Array(rows.unwrap_or_default());
1846 }
1847 if name == "id" {
1851 return Value::Str(self.repo_id.clone());
1852 }
1853 Target::parse(name).map_or(Value::Undefined, scan_marker)
1856 }
1857
1858 fn root_object(&self) -> Value {
1861 self.root_row()
1862 }
1863
1864 fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1865 if row.is_absent() {
1866 return Ok(Value::Undefined);
1867 }
1868 if is_root_object(row) {
1871 if key == "id" {
1872 return Ok(Value::Str(self.repo_id.clone()));
1873 }
1874 return Ok(Target::parse(key).map_or(Value::Undefined, scan_marker));
1875 }
1876 let Some(t) = target_of(row) else {
1877 return Ok(oqx::DefaultContext::read(row, key));
1879 };
1880 if let Some(full) = self.resolve_lazy(row, t, key)? {
1883 return self.get(&full, key);
1884 }
1885 if key == "$self" {
1890 return Ok(row.clone());
1891 }
1892 if attributes(t).contains(&key) {
1894 return self.attribute(row, t, key);
1895 }
1896 match (t, key) {
1898 (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1899 return Ok(row.clone());
1900 }
1901 (Target::Revisions, "doc") => return self.revision_doc(row),
1904 (_, "doc") => return self.owning_doc(row),
1905 (Target::Nodes, "block") => return self.owning_block(row),
1906 (Target::Links, "dst") => return self.destination_doc(row),
1907 (Target::Links, "block") => return self.link_block(row),
1908 _ => {}
1909 }
1910 if let Some(v) = self.relation(row, t, key)? {
1911 return Ok(v);
1912 }
1913 if let Some(named) = Target::parse(key) {
1920 return Err(OqxError::eval(crate::query::bare_target_message(
1921 named, None,
1922 )));
1923 }
1924 Ok(match t {
1926 Target::Docs => {
1927 let doc_id = col_str(row, "doc_id");
1928 if SOURCE_HANDLES.contains(&key) {
1933 let source = (key != "data").then_some(key);
1934 return self.doc_prop_bag(&doc_id, source);
1935 }
1936 return self.doc_prop(&doc_id, key, None);
1937 }
1938 Target::Blocks | Target::Nodes => Self::jattr(row, key),
1942 Target::Links | Target::Revisions => Value::Undefined,
1946 })
1947 }
1948
1949 fn to_rows(&self, value: &Value) -> Vec<Value> {
1950 match value {
1951 Value::Undefined | Value::Null => Vec::new(),
1952 Value::Array(a) => a.clone(),
1953 other if is_absent_single(other) => Vec::new(),
1954 other => match scan_of(other) {
1955 Some(t) => self.expand_scan(t),
1956 None => vec![other.clone()],
1957 },
1958 }
1959 }
1960
1961 fn is_single(&self, value: &Value) -> bool {
1965 scan_of(value).is_none()
1966 }
1967
1968 fn materialize(&self, value: Value) -> Value {
1973 if value.as_object().is_some_and(|o| o.len() == 1) {
1974 if let Some(t) = scan_of(&value) {
1975 return Value::Array(self.expand_scan(t));
1976 }
1977 if is_absent_single(&value) {
1978 return Value::Undefined;
1979 }
1980 }
1981 value
1982 }
1983
1984 fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
1985 crate::store_index::index_for(self, scan_of(collection)?, path)
1986 }
1987
1988 fn identity(&self, row: &Value) -> Value {
1989 match target_of(row) {
1990 Some(Target::Docs) => col(row, "doc_id").clone(),
1991 Some(Target::Blocks) => col(row, "block_id").clone(),
1992 Some(Target::Nodes) => col(row, "node_id").clone(),
1993 Some(Target::Links) => col(row, "edge_id").clone(),
1994 Some(Target::Revisions) => col(row, "rev_id").clone(),
1995 None => row.clone(),
1996 }
1997 }
1998
1999 fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
2000 if name == "refs" {
2003 return Some(self.refs(args.first().unwrap_or(&Value::Undefined)));
2004 }
2005 if let Some(r) = dates::date_function(name, args, self.now_ms) {
2010 return Some(r);
2011 }
2012 if name == "range" {
2013 let x = args.first().unwrap_or(&Value::Undefined);
2014 return Some(Ok(match x {
2015 Value::Range(_) => x.clone(),
2016 Value::Str(s) => match omgbase_properties::detect_range(s) {
2017 Some(r) => {
2018 let b = |b: &Bound| match b {
2019 Bound::Open => Value::Undefined,
2020 Bound::Num(n) => Value::Number(*n),
2021 Bound::Iso(s) => Value::Str(s.clone()),
2022 };
2023 Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
2024 }
2025 None => Value::Null,
2026 },
2027 _ => Value::Null,
2028 }));
2029 }
2030 builtin_function(name, args)
2031 }
2032
2033 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
2034 if let Some(t) = target_of(recv) {
2037 let full;
2039 let recv = if is_lazy_owner(recv) {
2040 let id_col = if t == Target::Docs {
2041 "doc_id"
2042 } else {
2043 "block_id"
2044 };
2045 match self.load_owner(t, &col_str(recv, id_col)) {
2046 Ok(Some(v)) => {
2047 full = v;
2048 &full
2049 }
2050 Ok(None) => recv,
2051 Err(e) => return Some(Err(e)),
2052 }
2053 } else {
2054 recv
2055 };
2056 if let Some(r) = self.row_method(name, recv, t, args) {
2057 return Some(r);
2058 }
2059 } else if matches!(
2060 name,
2061 "text"
2062 | "semantic"
2063 | "under"
2064 | "under_heading"
2065 | "within"
2066 | "under_kind"
2067 | "yaml_path"
2068 | "json_pointer"
2069 | "has_edge"
2070 | "has_anchor"
2071 | "child_count"
2072 | "parent_type"
2073 ) {
2074 return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/links row"));
2075 }
2076 if let Some(r) = dates::date_method(name, recv, args) {
2081 return Some(r);
2082 }
2083 if name == "matches" {
2084 if let Some(r) = self.matches_memoized(recv, args) {
2085 return Some(r);
2086 }
2087 }
2088 builtin_method_with(RegexDialect::Oqx, name, recv, args)
2089 }
2090}
2091
2092#[cfg(test)]
2093mod tests {
2094 use super::*;
2095
2096 #[test]
2097 fn glob_to_like_escapes() {
2098 assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
2099 assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
2100 assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
2101 }
2102
2103 #[test]
2104 fn rows_surfacing_as_values_render_id_and_path() {
2105 let mut node = Object::new();
2109 node.insert("node_id", Value::Str("n_1".into()));
2110 node.insert("attrs", Value::Str("{\"checked\":true}".into()));
2111 node.insert("__path", Value::Str("a.md".into()));
2112 let mut doc = Object::new();
2113 doc.insert("doc_id", Value::Str("d_0".into()));
2114 doc.insert("path", Value::Str("a.md".into()));
2115 doc.insert("blob", Value::Str("ff".into()));
2116 let mut record = Object::new();
2117 record.insert(TAG_KEY, Value::Str("junk".into()));
2118 record.insert(
2119 "tasks",
2120 Value::Array(vec![
2121 tag_row(node, Target::Nodes),
2122 tag_row(doc, Target::Docs),
2123 ]),
2124 );
2125 let out = render_row_values(Value::Object(record));
2126 let o = out.as_object().unwrap();
2127 assert!(o.get(TAG_KEY).is_none());
2128 let tasks = o.get("tasks").unwrap().as_array().unwrap();
2129 let keys = |v: &Value| -> Vec<String> {
2130 v.as_object()
2131 .unwrap()
2132 .iter()
2133 .map(|(k, _)| k.to_owned())
2134 .collect()
2135 };
2136 assert_eq!(keys(&tasks[0]), ["id", "path"]);
2137 assert_eq!(
2138 tasks[0].as_object().unwrap().get("id"),
2139 Some(&Value::Str("n_1".into()))
2140 );
2141 assert_eq!(
2142 tasks[0].as_object().unwrap().get("path"),
2143 Some(&Value::Str("/a.md".into()))
2144 );
2145 assert_eq!(keys(&tasks[1]), ["id", "path"]);
2146 assert_eq!(
2147 tasks[1].as_object().unwrap().get("id"),
2148 Some(&Value::Str("d_0".into()))
2149 );
2150 let mut edge = Object::new();
2152 edge.insert("edge_id", Value::Number(7.0));
2153 let e = render_row_values(tag_row(edge, Target::Links));
2154 assert_eq!(
2155 e.as_object().unwrap().get("id"),
2156 Some(&Value::Str("7".into()))
2157 );
2158 assert_eq!(
2159 e.as_object().unwrap().get("path"),
2160 Some(&Value::Str(String::new()))
2161 );
2162 }
2163
2164 #[test]
2165 fn nested_property_objects_rebuild() {
2166 let mut o = Object::new();
2167 set_nested(&mut o, &["a", "b"], Value::Number(1.0));
2168 set_nested(&mut o, &["a", "c"], Value::Number(2.0));
2169 set_nested(&mut o, &["d"], Value::Str("x".into()));
2170 let a = o.get("a").unwrap().as_object().unwrap();
2171 assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
2172 assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
2173 assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
2174 set_nested(&mut o, &["d", "e"], Value::Bool(true));
2176 assert!(o.get("d").unwrap().as_object().is_some());
2177 }
2178
2179 #[test]
2180 fn json_and_sql_bridges() {
2181 assert_eq!(
2182 parse_json(&Value::Str("{\"a\":1}".into()))
2183 .as_object()
2184 .unwrap()
2185 .get("a"),
2186 Some(&Value::Number(1.0))
2187 );
2188 assert_eq!(
2189 parse_json(&Value::Str("nope".into())),
2190 Value::Str("nope".into())
2191 );
2192 assert_eq!(parse_json(&Value::Null), Value::Undefined);
2193 assert_eq!(arg_or_empty(&[], 0), "");
2194 assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
2195 }
2196}