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