1use std::cell::{Cell, RefCell};
39use std::collections::HashMap;
40use std::rc::Rc;
41
42use omgbase_properties::Bound;
43use omgbase_search::{cosine_bytes, sanitize_fts_query};
44use oqx::semantics::{builtin_function, builtin_method_with, make_range, string_form};
45use oqx::{
46 CompiledRegex, DataContext, Object, OqxError, RegexDialect, RowIndex, Value, compile_regex,
47};
48use rusqlite::types::{Value as SqlValue, ValueRef};
49use rusqlite::{Connection, OptionalExtension, params_from_iter};
50
51use crate::paths::{reference_path, storage_path};
52use crate::planner;
53
54pub const TAG_KEY: &str = "__oqx_target";
56pub const SCAN_KEY: &str = "__oqx_scan";
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
75pub enum Target {
76 Docs,
77 Blocks,
78 Nodes,
79 Links,
80 Revisions,
81}
82
83impl Target {
84 #[must_use]
86 pub fn as_str(self) -> &'static str {
87 match self {
88 Target::Docs => "docs",
89 Target::Blocks => "blocks",
90 Target::Nodes => "nodes",
91 Target::Links => "links",
92 Target::Revisions => "revisions",
93 }
94 }
95
96 #[must_use]
98 pub fn parse(s: &str) -> Option<Self> {
99 Some(match s {
100 "docs" => Target::Docs,
101 "blocks" => Target::Blocks,
102 "nodes" => Target::Nodes,
103 "links" => Target::Links,
104 "revisions" => Target::Revisions,
105 _ => return None,
106 })
107 }
108
109 #[must_use]
112 pub fn noun(self) -> &'static str {
113 match self {
114 Target::Docs => "documents",
115 Target::Blocks => "blocks",
116 Target::Nodes => "nodes",
117 Target::Links => "links",
118 Target::Revisions => "revisions",
119 }
120 }
121}
122
123const DOC_ATTRIBUTES: [&str; 8] = [
145 "id",
146 "path",
147 "raw",
148 "hash",
149 "updated_at",
150 "title",
151 "tags",
152 "format",
153];
154const BLOCK_ATTRIBUTES: [&str; 11] = [
155 "id",
156 "path",
157 "raw",
158 "hash",
159 "updated_at",
160 "doc_id",
161 "type",
162 "text",
163 "attrs",
164 "depth",
165 "ordinal",
166];
167const NODE_ATTRIBUTES: [&str; 8] = [
168 "id", "path", "doc_id", "block_id", "kind", "name", "value", "attrs",
169];
170const EDGE_ATTRIBUTES: [&str; 13] = [
171 "id",
172 "path",
173 "doc_id",
174 "block_id",
175 "via_id",
176 "dst_id",
177 "dst_path",
178 "uri",
179 "field",
180 "dst_kind",
181 "predicate",
182 "provenance",
183 "anchor",
184];
185const REVISION_ATTRIBUTES: [&str; 12] = [
190 "id",
191 "path",
192 "rev",
193 "seq",
194 "doc_id",
195 "commit",
196 "ts",
197 "origin",
198 "actor",
199 "reason",
200 "hash",
201 "is_current",
202];
203#[must_use]
205pub fn attributes(t: Target) -> &'static [&'static str] {
206 match t {
207 Target::Docs => &DOC_ATTRIBUTES,
208 Target::Blocks => &BLOCK_ATTRIBUTES,
209 Target::Nodes => &NODE_ATTRIBUTES,
210 Target::Links => &EDGE_ATTRIBUTES,
211 Target::Revisions => &REVISION_ATTRIBUTES,
212 }
213}
214
215pub(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";
223pub(crate) const REVISION_FROM: &str =
224 "revisions r JOIN docs d ON d.doc_id = r.doc_id JOIN commits c ON c.commit_id = r.commit_id";
225pub const SOURCE_HANDLES: [&str; 3] = ["data", "frontmatter", "inline"];
228
229pub const LAZY_KEY: &str = "__oqx_lazy";
243
244pub const ABSENT_KEY: &str = "__oqx_absent";
252
253fn absent_single() -> Value {
254 let mut o = Object::with_capacity(1);
255 o.insert(ABSENT_KEY, Value::Bool(true));
256 Value::Object(o)
257}
258
259fn is_absent_single(v: &Value) -> bool {
261 v.as_object()
262 .is_some_and(|o| o.len() == 1 && o.contains_key(ABSENT_KEY))
263}
264
265#[derive(Clone, Debug, PartialEq)]
268pub struct SemanticVec {
269 pub model: String,
270 pub vec: Vec<u8>,
271}
272
273pub struct StoreContext<'a> {
275 conn: &'a Connection,
276 repo_id: String,
277 semantic: HashMap<String, SemanticVec>,
278 root_failure: RefCell<Option<OqxError>>,
281 rows_root: Option<RowsRoot>,
283 regexes: RefCell<HashMap<(String, Option<String>), CompiledRegex>>,
286 scans: RefCell<HashMap<Target, Rc<Vec<Value>>>>,
290 scans_run: Cell<usize>,
292 statements_run: Cell<usize>,
295 owners: RefCell<HashMap<(Target, String), Option<Value>>>,
298}
299
300struct RowsRoot {
305 rows: RefCell<Option<Vec<Value>>>,
306 once: bool,
307}
308
309fn sql_value(v: ValueRef<'_>) -> Value {
310 match v {
311 ValueRef::Null => Value::Null,
312 ValueRef::Integer(i) => Value::Number(i as f64),
313 ValueRef::Real(f) => Value::Number(f),
314 ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
315 ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
316 }
317}
318
319pub(crate) fn to_sql(v: &Value) -> SqlValue {
324 match v {
325 Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
326 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
327 Value::Number(n) => SqlValue::Real(*n),
328 Value::Str(s) => SqlValue::Text(s.clone()),
329 Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
330 }
331}
332
333fn js_string(v: &Value) -> String {
335 v.to_string()
336}
337
338fn arg_or_empty(args: &[Value], i: usize) -> String {
340 match args.get(i) {
341 None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
342 Some(v) => js_string(v),
343 }
344}
345
346fn parse_json(v: &Value) -> Value {
348 match v {
349 Value::Str(s) => {
350 serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
351 }
352 Value::Null | Value::Undefined => Value::Undefined,
353 other => other.clone(),
354 }
355}
356
357fn sql_err(e: rusqlite::Error) -> OqxError {
360 OqxError::eval(format!("sqlite: {e}"))
361}
362
363pub fn target_of(row: &Value) -> Option<Target> {
365 row.as_object()
366 .and_then(|o| o.get(TAG_KEY))
367 .and_then(Value::as_str)
368 .and_then(Target::parse)
369}
370
371fn is_lazy_owner(row: &Value) -> bool {
373 row.as_object().is_some_and(|o| o.contains_key(LAZY_KEY))
374}
375
376#[must_use]
378pub fn scan_of(v: &Value) -> Option<Target> {
379 v.as_object()
380 .and_then(|o| o.get(SCAN_KEY))
381 .and_then(Value::as_str)
382 .and_then(Target::parse)
383}
384
385fn scan_marker(t: Target) -> Value {
386 let mut o = Object::with_capacity(1);
387 o.insert(SCAN_KEY, Value::Str(t.as_str().to_owned()));
388 Value::Object(o)
389}
390
391pub const ALL_TARGETS: [Target; 5] = [
394 Target::Docs,
395 Target::Blocks,
396 Target::Nodes,
397 Target::Links,
398 Target::Revisions,
399];
400
401fn is_root_object(row: &Value) -> bool {
406 row.as_object().is_some_and(|o| {
407 o.len() == ALL_TARGETS.len()
408 && ALL_TARGETS
409 .iter()
410 .all(|t| o.get(t.as_str()).is_some_and(|v| scan_of(v) == Some(*t)))
411 })
412}
413
414fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
415 row.as_object()
416 .and_then(|o| o.get(key))
417 .unwrap_or(&Value::Undefined)
418}
419
420fn col_str(row: &Value, key: &str) -> String {
421 match col(row, key) {
422 Value::Undefined | Value::Null => String::new(),
423 v => js_string(v),
424 }
425}
426
427#[must_use]
429pub fn strip_tags(v: Value) -> Value {
430 match v {
431 Value::Object(o) => Value::Object(
432 o.into_iter()
433 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
434 .map(|(k, x)| (k, strip_tags(x)))
435 .collect(),
436 ),
437 Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
438 other => other,
439 }
440}
441
442#[must_use]
451pub fn render_row_values(v: Value) -> Value {
452 match v {
453 Value::Object(o) => {
454 let row = Value::Object(o);
455 if let Some(t) = target_of(&row) {
456 let (id_col, path_col) = match t {
457 Target::Docs => ("doc_id", "path"),
458 Target::Blocks => ("block_id", "__path"),
459 Target::Nodes => ("node_id", "__path"),
460 Target::Links => ("edge_id", "__path"),
461 Target::Revisions => ("rev_id", "path"),
464 };
465 let mut out = Object::with_capacity(2);
466 out.insert(crate::query::HIT_ID, Value::Str(col_str(&row, id_col)));
467 let path = match col(&row, path_col) {
468 Value::Undefined | Value::Null => String::new(),
469 v => reference_path(&js_string(v)),
470 };
471 out.insert(crate::query::HIT_PATH, Value::Str(path));
472 return Value::Object(out);
473 }
474 let Value::Object(o) = row else {
475 unreachable!()
476 };
477 Value::Object(
478 o.into_iter()
479 .filter(|(k, _)| k != TAG_KEY && k != LAZY_KEY)
480 .map(|(k, x)| (k, render_row_values(x)))
481 .collect(),
482 )
483 }
484 Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
485 other => other,
486 }
487}
488
489impl<'a> StoreContext<'a> {
490 #[must_use]
493 pub fn new(
494 conn: &'a Connection,
495 repo_id: &str,
496 semantic: HashMap<String, SemanticVec>,
497 ) -> Self {
498 Self {
499 conn,
500 repo_id: repo_id.to_owned(),
501 semantic,
502 root_failure: RefCell::new(None),
503 rows_root: None,
504 regexes: RefCell::new(HashMap::new()),
505 scans: RefCell::new(HashMap::new()),
506 scans_run: Cell::new(0),
507 statements_run: Cell::new(0),
508 owners: RefCell::new(HashMap::new()),
509 }
510 }
511
512 #[must_use]
515 pub fn statements_run(&self) -> usize {
516 self.statements_run.get()
517 }
518
519 #[must_use]
521 pub fn repo_id(&self) -> &str {
522 &self.repo_id
523 }
524
525 #[must_use]
529 pub fn scans_run(&self) -> usize {
530 self.scans_run.get()
531 }
532
533 pub(crate) fn scan_rows(&self, t: Target) -> oqx::Result<Rc<Vec<Value>>> {
535 if let Some(rows) = self.scans.borrow().get(&t) {
536 return Ok(Rc::clone(rows));
537 }
538 self.scans_run.set(self.scans_run.get() + 1);
539 let rows = match self.root_scan(t)? {
540 Value::Array(rows) => Rc::new(rows),
541 _ => Rc::new(Vec::new()),
542 };
543 self.scans.borrow_mut().insert(t, Rc::clone(&rows));
544 Ok(rows)
545 }
546
547 fn refs(&self, x: &Value) -> oqx::Result<Value> {
555 let items: Vec<&Value> = match x {
556 Value::Undefined | Value::Null => Vec::new(),
557 Value::Array(a) => a.iter().collect(),
558 other => vec![other],
559 };
560 let mut out = Vec::new();
561 for item in items {
562 if let Value::Str(s) = item
563 && let Some(row) = self.ref_doc(s)?
564 {
565 out.push(row);
566 }
567 }
568 Ok(Value::Array(out))
569 }
570
571 fn ref_doc(&self, r: &str) -> oqx::Result<Option<Value>> {
577 let t = Target::Docs;
578 let lookup = |column: &str, value: &str| -> oqx::Result<Option<Value>> {
579 let sql = format!(
580 "SELECT {} FROM {} WHERE {} AND d.{column} = ? ORDER BY {}",
581 planner::columns(t),
582 planner::from_clause(t),
583 planner::guards(t),
584 planner::order_clause(t)
585 );
586 let params = [
587 SqlValue::Text(self.repo_id.clone()),
588 SqlValue::Text(value.to_owned()),
589 ];
590 Ok(self.one(&sql, ¶ms)?.map(|row| tag_row(row, t)))
591 };
592 if let Some(row) = lookup("path", storage_path(r))? {
593 return Ok(Some(row));
594 }
595 if r.starts_with("d_") {
596 return lookup("doc_id", r);
597 }
598 Ok(None)
599 }
600
601 pub(crate) fn probe_rows(
603 &self,
604 t: Target,
605 sql: &str,
606 params: &[SqlValue],
607 ) -> oqx::Result<Vec<Value>> {
608 Ok(tag_rows(self.all(sql, params)?, t))
609 }
610
611 fn expand_scan(&self, t: Target) -> Vec<Value> {
614 match self.scan_rows(t) {
615 Ok(rows) => rows.as_ref().clone(),
616 Err(e) => {
617 let mut slot = self.root_failure.borrow_mut();
618 if slot.is_none() {
619 *slot = Some(e);
620 }
621 Vec::new()
622 }
623 }
624 }
625
626 #[must_use]
631 pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
632 self.rows_root = Some(RowsRoot {
633 rows: RefCell::new(Some(rows)),
634 once: false,
635 });
636 self
637 }
638
639 #[must_use]
647 pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
648 self.rows_root = Some(RowsRoot {
649 rows: RefCell::new(Some(rows)),
650 once: true,
651 });
652 self
653 }
654
655 pub fn take_root_failure(&self) -> Option<OqxError> {
660 self.root_failure.borrow_mut().take()
661 }
662
663 fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
666 self.statements_run.set(self.statements_run.get() + 1);
667 fetch_rows(self.conn, sql, params).map_err(sql_err)
668 }
669
670 fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
671 Ok(self.all(sql, params)?.into_iter().next())
672 }
673
674 fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
675 Ok(self
676 .one(sql, params)?
677 .and_then(|o| o.values().next().cloned())
678 .unwrap_or(Value::Undefined))
679 }
680
681 fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
682 self.statements_run.set(self.statements_run.get() + 1);
683 let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
684 stmt.exists(params_from_iter(params.iter()))
685 .map_err(sql_err)
686 }
687
688 fn tag_all(rows: Vec<Object>, t: Target) -> Value {
689 Value::Array(tag_rows(rows, t))
690 }
691
692 fn tag(row: Object, t: Target) -> Value {
693 tag_row(row, t)
694 }
695
696 fn root_row(&self) -> Value {
704 let mut o = Object::with_capacity(ALL_TARGETS.len());
705 for t in ALL_TARGETS {
706 o.insert(t.as_str(), scan_marker(t));
707 }
708 Value::Object(o)
709 }
710
711 fn root_scan(&self, t: Target) -> oqx::Result<Value> {
724 let repo = [SqlValue::Text(self.repo_id.clone())];
725 let sql = match t {
726 Target::Docs => {
727 "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
728 }
729 Target::Blocks => {
730 "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
731 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
732 ORDER BY d.path, b.block_id"
733 }
734 Target::Nodes => {
735 "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
736 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
737 }
738 Target::Links => {
739 "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
740 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
741 ORDER BY d.path, e.edge_id"
742 }
743 Target::Revisions => {
746 let sql = format!(
747 "SELECT {REVISION_COLS} FROM {REVISION_FROM} WHERE d.repo_id = ?1 ORDER BY r.path, r.rev_id"
748 );
749 return Ok(Self::tag_all(self.all(&sql, &repo)?, t));
750 }
751 };
752 Ok(Self::tag_all(self.all(sql, &repo)?, t))
753 }
754
755 fn doc_revisions(&self, doc_id: &str) -> oqx::Result<Value> {
758 let sql = format!(
759 "SELECT {REVISION_COLS} FROM {REVISION_FROM} WHERE r.doc_id = ?1 ORDER BY r.seq"
760 );
761 Ok(Self::tag_all(
762 self.all(&sql, &[SqlValue::Text(doc_id.to_owned())])?,
763 Target::Revisions,
764 ))
765 }
766
767 fn decode_prop(r: &Object) -> Value {
772 let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
773 match get("type").as_str().unwrap_or("") {
774 "number" => get("val_num"),
775 "bool" => Value::Bool(get("val_bool").truthy()),
776 "null" => Value::Null,
777 "json" => parse_json(&get("val_json")),
778 _ => get("val_text"),
779 }
780 }
781
782 fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
785 let rows = match source {
786 Some(s) => self.all(
787 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
788 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
789 )?,
790 None => self.all(
791 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
792 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
793 )?,
794 };
795 if rows.is_empty() {
796 return self.doc_prop_object(doc_id, key, source);
797 }
798 if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
799 return Ok(Self::decode_prop(&rows[0]));
800 }
801 Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
802 }
803
804 fn doc_prop_object(
807 &self,
808 doc_id: &str,
809 prefix: &str,
810 source: Option<&str>,
811 ) -> oqx::Result<Value> {
812 let like = SqlValue::Text(format!("{prefix}.%"));
813 let rows = match source {
814 Some(s) => self.all(
815 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
816 &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
817 )?,
818 None => self.all(
819 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
820 &[SqlValue::Text(doc_id.to_owned()), like],
821 )?,
822 };
823 if rows.is_empty() {
824 return Ok(Value::Undefined);
825 }
826 let mut out = Object::new();
827 for r in &rows {
828 let key = r.get("key").and_then(Value::as_str).unwrap_or("");
829 let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
830 .split('.')
831 .collect();
832 set_nested(&mut out, &rest, Self::decode_prop(r));
833 }
834 Ok(Value::Object(out))
835 }
836
837 fn doc_prop_bag(&self, doc_id: &str, source: Option<&str>) -> oqx::Result<Value> {
842 let keys = match source {
843 Some(s) => self.all(
844 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
845 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(s.to_owned())],
846 )?,
847 None => self.all(
848 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source IN ('frontmatter', 'inline') AND deleted_commit IS NULL ORDER BY key",
849 &[SqlValue::Text(doc_id.to_owned())],
850 )?,
851 };
852 let mut out = Object::new();
853 for k in keys {
854 let key = k.get("key").and_then(Value::as_str).unwrap_or("");
855 let top = key.split('.').next().unwrap_or("");
856 if !out.contains_key(top) {
857 let v = self.doc_prop(doc_id, top, source)?;
858 out.insert(top, v);
859 }
860 }
861 Ok(Value::Object(out))
862 }
863
864 fn doc_tags(&self, doc_id: &str) -> oqx::Result<Value> {
870 fn as_list(v: Value) -> Vec<Value> {
871 match v {
872 Value::Undefined | Value::Null => Vec::new(),
873 Value::Array(a) => a,
874 other => vec![other],
875 }
876 }
877 let authored = as_list(self.doc_prop(doc_id, "tags", None)?);
878 let hashtags = as_list(self.doc_prop(doc_id, "$tags", Some("computed"))?);
879 if authored.is_empty() && hashtags.is_empty() {
880 return Ok(Value::Null);
881 }
882 let mut seen: Vec<String> = Vec::new();
883 let mut out = Vec::new();
884 for v in authored.into_iter().chain(hashtags) {
885 let k = v.to_canonical_json().to_string();
886 if seen.contains(&k) {
887 continue;
888 }
889 seen.push(k);
890 out.push(v);
891 }
892 Ok(Value::Array(out))
893 }
894
895 fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
900 let ordinal = col(block, "ordinal").clone();
901 if col(block, "parent_block").is_absent() {
902 return Ok(ordinal);
903 }
904 let ap = col_str(block, "ancestor_path");
905 let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
906 return Ok(ordinal);
907 };
908 let r = self.scalar(
909 "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
910 &[
911 SqlValue::Text(col_str(block, "doc_id")),
912 SqlValue::Text(first.to_owned()),
913 ],
914 )?;
915 Ok(if r.is_absent() { ordinal } else { r })
916 }
917
918 fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
922 let rows = self.all(
923 "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",
924 &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
925 )?;
926 let ids: Vec<String> = rows
927 .iter()
928 .map(|r| {
929 r.get("block_id")
930 .and_then(Value::as_str)
931 .unwrap_or("")
932 .to_owned()
933 })
934 .collect();
935 let parent_index: Vec<Option<usize>> = rows
936 .iter()
937 .map(|r| {
938 r.get("parent_block")
939 .and_then(Value::as_str)
940 .and_then(|p| ids.iter().position(|id| id == p))
941 })
942 .collect();
943 let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
944 let mut roots = Vec::new();
945 for (i, p) in parent_index.iter().enumerate() {
946 match p {
947 Some(p) => children[*p].push(i),
948 None => roots.push(i),
949 }
950 }
951 fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
952 order.push(i);
953 for &c in &children[i] {
954 walk(c, children, order);
955 }
956 }
957 let mut order = Vec::with_capacity(rows.len());
958 for r in roots {
959 walk(r, &children, &mut order);
960 }
961 let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
962 Ok(order
963 .into_iter()
964 .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
965 .collect())
966 }
967
968 fn jattr(row: &Value, k: &str) -> Value {
969 match parse_json(col(row, "attrs")) {
970 Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
971 _ => Value::Undefined,
972 }
973 }
974
975 fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
977 let path = || SqlValue::Text(col_str(row, "__path"));
978 let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
979 let doc_path = || SqlValue::Text(col_str(row, "path"));
980 let block_id = || SqlValue::Text(col_str(row, "block_id"));
981 Ok(Some(match (t, key) {
982 (Target::Docs, "nodes") => {
983 let rows = self.all(
986 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
987 &[doc_path(), doc_id()],
988 )?;
989 let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
990 let rank: HashMap<String, usize> = blocks
991 .iter()
992 .enumerate()
993 .map(|(i, b)| (col_str(b, "block_id"), i))
994 .collect();
995 let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
996 .into_iter()
997 .map(|r| {
998 let block = r.get("block_id").and_then(Value::as_str);
999 let (has_block, rk) = match block {
1000 None => (0, 0),
1001 Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
1002 };
1003 let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
1004 let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
1005 ((has_block, rk, span, id), r)
1006 })
1007 .collect();
1008 keyed.sort_by(|a, b| {
1009 a.0.0
1010 .cmp(&b.0.0)
1011 .then(a.0.1.cmp(&b.0.1))
1012 .then(a.0.2.total_cmp(&b.0.2))
1013 .then(a.0.3.cmp(&b.0.3))
1014 });
1015 Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
1016 }
1017 (Target::Docs, "blocks") => {
1018 Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
1019 }
1020 (Target::Docs, "revisions") => self.doc_revisions(&col_str(row, "doc_id"))?,
1022 (Target::Docs, "out_links") => Self::tag_all(
1027 self.all(
1028 "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",
1029 &[doc_path(), doc_id()],
1030 )?,
1031 Target::Links,
1032 ),
1033 (Target::Docs, "in_links") => Self::tag_all(
1034 self.all(
1035 "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
1036 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
1037 &[doc_id()],
1038 )?,
1039 Target::Links,
1040 ),
1041 (Target::Blocks, "children") => Self::tag_all(
1042 self.all(
1043 "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",
1044 &[path(), block_id()],
1045 )?,
1046 Target::Blocks,
1047 ),
1048 (Target::Blocks, "nodes") => Self::tag_all(
1049 self.all(
1050 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
1051 &[path(), block_id()],
1052 )?,
1053 Target::Nodes,
1054 ),
1055 (Target::Blocks, "out_links") => Self::tag_all(
1056 self.all(
1057 "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",
1058 &[path(), block_id()],
1059 )?,
1060 Target::Links,
1061 ),
1062 (Target::Blocks, "section") => {
1063 let top = to_sql(&self.top_ordinal(row)?);
1064 Self::tag_all(
1065 self.all(
1066 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
1067 AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
1068 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1069 &[path(), doc_id(), top.clone(), top],
1070 )?,
1071 Target::Nodes,
1072 )
1073 }
1074 (Target::Nodes, "blocks") => {
1075 let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
1076 if f.is_absent() || l.is_absent() {
1077 return Ok(Some(Value::Array(Vec::new())));
1078 }
1079 let (f, l) = (
1080 f.as_f64().unwrap_or(f64::NAN),
1081 l.as_f64().unwrap_or(f64::NAN),
1082 );
1083 let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
1084 let mut kept: Vec<Value> = Vec::new();
1085 for b in rows {
1086 let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
1087 if t >= f && t <= l {
1088 kept.push(b);
1089 }
1090 }
1091 Value::Array(kept)
1092 }
1093 (Target::Nodes, "subsections") => {
1094 let (f, l, lvl) = (
1095 Self::jattr(row, "first_ordinal"),
1096 Self::jattr(row, "last_ordinal"),
1097 Self::jattr(row, "level"),
1098 );
1099 if f.is_absent() {
1100 return Ok(Some(Value::Array(Vec::new())));
1101 }
1102 Self::tag_all(
1103 self.all(
1104 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
1105 AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
1106 AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
1107 &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1108 )?,
1109 Target::Nodes,
1110 )
1111 }
1112 (Target::Nodes, "children") => {
1113 let (f, l, lvl) = (
1114 Self::jattr(row, "first_ordinal"),
1115 Self::jattr(row, "last_ordinal"),
1116 Self::jattr(row, "level"),
1117 );
1118 if f.is_absent() {
1119 return Ok(Some(Value::Array(Vec::new())));
1120 }
1121 Self::tag_all(
1122 self.all(
1123 "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
1124 AND json_extract(i.attrs,'$.level') > ?3
1125 AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
1126 AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
1127 AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
1128 AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
1129 AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
1130 ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
1131 &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
1132 )?,
1133 Target::Nodes,
1134 )
1135 }
1136 _ => return Ok(None),
1137 }))
1138 }
1139
1140 fn lazy_owner(known: &[(&str, Value)], t: Target) -> Value {
1153 let mut o = Object::with_capacity(known.len() + 2);
1154 for (k, v) in known {
1155 o.insert(*k, v.clone());
1156 }
1157 o.insert(LAZY_KEY, Value::Bool(true));
1158 tag_row(o, t)
1159 }
1160
1161 fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
1162 let id = match col(row, "doc_id") {
1163 Value::Undefined | Value::Null => col(row, "src_doc").clone(),
1164 v => v.clone(),
1165 };
1166 if id.is_absent() {
1167 return Ok(absent_single());
1168 }
1169 match col(row, "__path") {
1170 Value::Str(path) => Ok(Self::lazy_owner(
1171 &[("doc_id", id), ("path", Value::Str(path.clone()))],
1172 Target::Docs,
1173 )),
1174 _ => Ok(self
1175 .load_owner(Target::Docs, &js_string(&id))?
1176 .unwrap_or_else(absent_single)),
1177 }
1178 }
1179
1180 fn destination_doc(&self, row: &Value) -> oqx::Result<Value> {
1187 if col(row, "dst_kind") != &Value::Str("document".to_owned()) {
1188 return Ok(absent_single());
1189 }
1190 Ok(self
1191 .load_owner(Target::Docs, &col_str(row, "dst_node"))?
1192 .unwrap_or_else(absent_single))
1193 }
1194
1195 fn revision_doc(&self, row: &Value) -> oqx::Result<Value> {
1202 Ok(self
1203 .one(
1204 "SELECT * FROM docs WHERE doc_id = ?1 AND deleted_commit IS NULL",
1205 &[SqlValue::Text(col_str(row, "doc_id"))],
1206 )?
1207 .map_or_else(absent_single, |o| Self::tag(o, Target::Docs)))
1208 }
1209
1210 fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
1211 let id = col(row, "block_id");
1212 if !id.truthy() {
1213 return Ok(absent_single());
1214 }
1215 match col(row, "__path") {
1216 Value::Str(path) => Ok(Self::lazy_owner(
1217 &[
1218 ("block_id", id.clone()),
1219 ("doc_id", col(row, "doc_id").clone()),
1220 ("__path", Value::Str(path.clone())),
1221 ],
1222 Target::Blocks,
1223 )),
1224 _ => Ok(self
1225 .load_owner(Target::Blocks, &js_string(id))?
1226 .unwrap_or_else(absent_single)),
1227 }
1228 }
1229
1230 fn load_owner(&self, t: Target, id: &str) -> oqx::Result<Option<Value>> {
1233 let key = (t, id.to_owned());
1234 if let Some(v) = self.owners.borrow().get(&key) {
1235 return Ok(v.clone());
1236 }
1237 let loaded = match t {
1238 Target::Docs => self
1239 .one(
1240 "SELECT * FROM docs WHERE doc_id = ?1",
1241 &[SqlValue::Text(id.to_owned())],
1242 )?
1243 .map(|o| Self::tag(o, Target::Docs)),
1244 _ => self
1245 .one(
1246 "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
1247 &[SqlValue::Text(id.to_owned())],
1248 )?
1249 .map(|o| Self::tag(o, Target::Blocks)),
1250 };
1251 self.owners.borrow_mut().insert(key, loaded.clone());
1252 Ok(loaded)
1253 }
1254
1255 fn lazy_needs_load(t: Target, key: &str) -> bool {
1265 match t {
1266 Target::Docs => matches!(key, "format" | "hash" | "updated_at"),
1267 Target::Blocks => {
1268 !matches!(
1269 key,
1270 "id" | "path"
1271 | "updated_at"
1272 | "$self"
1273 | "doc_id"
1274 | "block"
1275 | "doc"
1276 | "children"
1277 | "nodes"
1278 | "out_links"
1279 ) && Target::parse(key).is_none()
1280 }
1281 Target::Nodes | Target::Links | Target::Revisions => true,
1283 }
1284 }
1285
1286 fn resolve_lazy(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
1290 if !is_lazy_owner(row) || !Self::lazy_needs_load(t, key) {
1291 return Ok(None);
1292 }
1293 let id_col = if t == Target::Docs {
1294 "doc_id"
1295 } else {
1296 "block_id"
1297 };
1298 self.load_owner(t, &col_str(row, id_col))
1299 }
1300
1301 fn null_if_absent(v: Value) -> Value {
1304 if v.is_absent() { Value::Null } else { v }
1305 }
1306
1307 fn attribute(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
1316 let c = |k: &str| col(row, k).clone();
1317 let rooted = |k: &str| Value::Str(reference_path(&col_str(row, k)));
1318 Ok(match (t, name) {
1319 (Target::Docs, "id") => c("doc_id"),
1320 (Target::Docs, "path") => rooted("path"),
1321 (Target::Docs, "hash") => Self::null_if_absent(c("file_hash")),
1322 (Target::Docs, "updated_at") => Self::null_if_absent(self.scalar(
1323 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
1324 &[to_sql(&c("current_rev"))],
1325 )?),
1326 (Target::Docs, "raw") => {
1328 match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
1329 Ok(Some(s)) => Value::Str(s),
1330 Ok(None) => Value::Null,
1331 Err(e) => return Err(OqxError::eval(e.to_string())),
1332 }
1333 }
1334 (Target::Docs, "title") => {
1339 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
1340 }
1341 (Target::Docs, "tags") => self.doc_tags(&col_str(row, "doc_id"))?,
1344 (Target::Docs, "format") => c("format"),
1345 (Target::Blocks, "id") => c("block_id"),
1346 (Target::Blocks, "path") => rooted("__path"),
1347 (Target::Blocks, "raw") => match c("raw_hash") {
1351 Value::Str(hex) => {
1352 let hash = omgbase_store::tree::from_hex(&hex)
1353 .map_err(|e| OqxError::eval(e.to_string()))?;
1354 let bytes: Option<Vec<u8>> = self
1355 .conn
1356 .query_row(
1357 "SELECT bytes FROM blobs WHERE hash = ?1",
1358 rusqlite::params![hash],
1359 |r| r.get(0),
1360 )
1361 .optional()
1362 .map_err(sql_err)?;
1363 self.statements_run.set(self.statements_run.get() + 1);
1364 bytes.map_or(Value::Null, |b| {
1365 Value::Str(String::from_utf8_lossy(&b).into_owned())
1366 })
1367 }
1368 _ => Value::Null,
1369 },
1370 (Target::Blocks, "hash") => Self::null_if_absent(c("raw_hash")),
1371 (Target::Blocks, "updated_at") => Self::null_if_absent(self.scalar(
1372 "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
1373 &[to_sql(&c("block_id"))],
1374 )?),
1375 (Target::Blocks, "doc_id" | "type" | "text" | "ordinal" | "depth") => c(name),
1378 (Target::Blocks | Target::Nodes, "attrs") => parse_json(&c("attrs")),
1379 (Target::Nodes, "id") => c("node_id"),
1380 (Target::Nodes, "path") => rooted("__path"),
1381 (Target::Nodes, "doc_id" | "kind" | "name" | "value") => c(name),
1382 (Target::Nodes, "block_id") => Self::null_if_absent(c("block_id")),
1383 (Target::Links, "id") => c("edge_id"),
1386 (Target::Links, "path") => rooted("__path"),
1387 (Target::Links, "doc_id") => c("src_doc"),
1388 (Target::Links, "block_id") => Self::null_if_absent(c("src_block")),
1389 (Target::Links, "via_id") => Self::null_if_absent(c("via_node")),
1390 (Target::Links, "dst_id") => c("dst_node"),
1391 (Target::Links, "dst_path") => match self.scalar(
1392 "SELECT path FROM docs WHERE doc_id = ?1",
1393 &[to_sql(&c("dst_node"))],
1394 )? {
1395 Value::Undefined | Value::Null => Value::Null,
1396 p => Value::Str(reference_path(&js_string(&p))),
1397 },
1398 (Target::Links, "uri") => Self::null_if_absent(self.scalar(
1399 "SELECT uri FROM external_nodes WHERE node_id = ?1",
1400 &[to_sql(&c("dst_node"))],
1401 )?),
1402 (Target::Links, "field") => Self::null_if_absent(c("src_field")),
1403 (Target::Links, "predicate" | "provenance" | "dst_kind" | "anchor") => c(name),
1404 (Target::Revisions, "id" | "rev") => c("rev_id"),
1408 (Target::Revisions, "path") => rooted("path"),
1409 (Target::Revisions, "seq" | "doc_id" | "ts" | "origin") => c(name),
1410 (Target::Revisions, "commit") => c("commit_id"),
1411 (Target::Revisions, "actor" | "reason") => Self::null_if_absent(c(name)),
1412 (Target::Revisions, "hash") => c("rendered_hash"),
1413 (Target::Revisions, "is_current") => Value::Bool(matches!(
1414 c("is_current"),
1415 Value::Number(n) if n == 1.0
1416 )),
1417 _ => Value::Undefined,
1418 })
1419 }
1420
1421 fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
1424 Some(Err(OqxError::eval(msg)))
1425 }
1426
1427 fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
1428 (t != want).then(|| {
1429 Err(OqxError::eval(format!(
1430 "{name}() is only available on the {} target",
1431 want.as_str()
1432 )))
1433 })
1434 }
1435
1436 fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
1437 r.map(Value::Bool)
1438 }
1439
1440 fn row_method(
1441 &self,
1442 name: &str,
1443 row: &Value,
1444 t: Target,
1445 args: &[Value],
1446 ) -> Option<oqx::Result<Value>> {
1447 let c = |k: &str| col(row, k).clone();
1448 match name {
1449 "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
1450 "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
1451 "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1452 Some(Self::sql_result(self.exists(
1453 "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
1454 &[to_sql(&c("block_id"))],
1455 )))
1456 }),
1457 "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1458 Some(self.scalar(
1459 "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
1460 &[to_sql(&c("block_id"))],
1461 ))
1462 }),
1463 "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1464 Some(
1465 self.scalar(
1466 "SELECT type FROM blocks WHERE block_id = ?1",
1467 &[to_sql(&c("parent_block"))],
1468 )
1469 .map(Self::null_if_absent),
1470 )
1471 }),
1472 "has_edge" => {
1473 let pred = js_string(args.first().unwrap_or(&Value::Undefined));
1474 let (src_col, src_val) = if t == Target::Blocks {
1475 ("src_block", c("block_id"))
1476 } else {
1477 ("src_doc", c("doc_id"))
1478 };
1479 let r = if args.len() >= 2 {
1480 self.exists(
1481 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
1482 &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
1483 )
1484 } else {
1485 self.exists(
1486 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
1487 &[to_sql(&src_val), SqlValue::Text(pred)],
1488 )
1489 };
1490 Some(Self::sql_result(r))
1491 }
1492 "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1493 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1494 let ap = col_str(row, "ancestor_path");
1495 Some(Ok(Value::Bool(
1496 ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
1497 )))
1498 }),
1499 "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1500 let text = js_string(args.first().unwrap_or(&Value::Undefined));
1501 let top = match self.top_ordinal(row) {
1502 Ok(v) => to_sql(&v),
1503 Err(e) => return Some(Err(e)),
1504 };
1505 Some(Self::sql_result(self.exists(
1506 "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
1507 WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
1508 &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
1509 )))
1510 }),
1511 "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1514 let target = js_string(args.first().unwrap_or(&Value::Undefined));
1515 if target.starts_with("d_") {
1516 return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
1517 }
1518 let target = storage_path(&target).to_owned();
1519 if target.contains('*') {
1520 let like = glob_to_like(&target, false);
1521 return Some(Self::sql_result(self.exists(
1522 "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
1523 &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
1524 )));
1525 }
1526 Some(Ok(Value::Bool(col_str(row, "__path") == target)))
1527 }),
1528 "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1529 let kind = js_string(args.first().unwrap_or(&Value::Undefined));
1530 let ap: Vec<String> = col_str(row, "ancestor_path")
1531 .split('/')
1532 .filter(|s| !s.is_empty())
1533 .map(str::to_owned)
1534 .collect();
1535 if ap.is_empty() {
1536 return Some(Ok(Value::Bool(false)));
1537 }
1538 let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
1539 let placeholders = placeholders.join(",");
1540 let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
1541 let n = params.len();
1542 params.push(SqlValue::Text(kind));
1543 let r = match args.get(1) {
1544 Some(v) if !v.is_absent() => {
1545 let nm = js_string(v);
1546 params.push(SqlValue::Text(nm.clone()));
1547 params.push(SqlValue::Text(nm));
1548 self.exists(
1549 &format!(
1550 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
1551 n + 1,
1552 n + 2,
1553 n + 3
1554 ),
1555 ¶ms,
1556 )
1557 }
1558 _ => self.exists(
1559 &format!(
1560 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
1561 n + 1
1562 ),
1563 ¶ms,
1564 ),
1565 };
1566 Some(Self::sql_result(r))
1567 }),
1568 "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1569 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
1570 }),
1571 "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
1572 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
1573 }),
1574 _ => None,
1575 }
1576 }
1577
1578 fn key_path(row: &Value, path: &str, kind: &str) -> Value {
1579 let key = if kind == "json" {
1580 let mut p = path;
1581 p = p.strip_prefix('#').unwrap_or(p);
1582 p = p.strip_prefix('/').unwrap_or(p);
1583 p.split('/').collect::<Vec<_>>().join(".")
1584 } else {
1585 path.to_owned()
1586 };
1587 let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
1588 if !col_str(row, "type").starts_with(&format!("{kind}:")) {
1589 return Value::Bool(false);
1590 }
1591 let k = Self::jattr(row, "key");
1592 Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
1593 }
1594
1595 fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
1596 if matches!(t, Target::Links | Target::Revisions) {
1598 return Err(OqxError::eval(format!(
1599 "text(...) is not available on the {} target",
1600 t.as_str()
1601 )));
1602 }
1603 let m = sanitize_fts_query(terms);
1604 if m.is_empty() {
1605 return Ok(Value::Bool(false));
1606 }
1607 let r = match t {
1608 Target::Docs => self.exists(
1609 "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",
1610 &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1611 ),
1612 Target::Nodes => self.exists(
1613 "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1614 &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1615 ),
1616 _ => self.exists(
1617 "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1618 &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1619 ),
1620 };
1621 Self::sql_result(r)
1622 }
1623
1624 fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1634 let flags = match args.get(1) {
1635 None | Some(Value::Undefined) | Some(Value::Null) => None,
1636 Some(Value::Str(s)) => Some(s.clone()),
1637 Some(_) => return None,
1638 };
1639 let Some(subject) = string_form(recv) else {
1640 return Some(Ok(Value::Bool(false)));
1641 };
1642 let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1643 let key = (pattern, flags);
1644 let mut memo = self.regexes.borrow_mut();
1645 if !memo.contains_key(&key) {
1646 let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1647 match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1648 Ok(re) => {
1649 memo.insert(key.clone(), re);
1650 }
1651 Err(_) => return None,
1652 }
1653 }
1654 Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1655 }
1656
1657 fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1658 if matches!(t, Target::Nodes | Target::Links | Target::Revisions) {
1659 return Err(OqxError::eval(
1660 "semantic(...) is available on the docs and blocks targets",
1661 ));
1662 }
1663 let Some(resolved) = self.semantic.get(phrase) else {
1664 return Err(OqxError::eval(format!(
1665 "semantic({}) needs an embedding provider; none is configured for this query",
1666 serde_json::Value::String(phrase.to_owned())
1667 )));
1668 };
1669 let vec: oqx::Result<Option<Vec<u8>>> = match t {
1670 Target::Docs => self
1671 .conn
1672 .query_row(
1673 "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1674 rusqlite::params![col_str(row, "doc_id"), resolved.model],
1675 |r| r.get(0),
1676 )
1677 .optional()
1678 .map_err(sql_err),
1679 _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1682 .map_err(|e| OqxError::eval(e.to_string())),
1683 };
1684 match vec? {
1685 Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1686 None => Ok(Value::Null),
1687 }
1688 }
1689}
1690
1691pub(crate) fn fetch_rows(
1694 conn: &Connection,
1695 sql: &str,
1696 params: &[SqlValue],
1697) -> rusqlite::Result<Vec<Object>> {
1698 let mut stmt = conn.prepare_cached(sql)?;
1699 let names: Vec<String> = stmt
1700 .column_names()
1701 .iter()
1702 .map(|s| (*s).to_owned())
1703 .collect();
1704 let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1705 let mut o = Object::with_capacity(names.len() + 1);
1708 for (i, name) in names.iter().enumerate() {
1709 o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1710 }
1711 Ok(o)
1712 })?;
1713 rows.collect()
1714}
1715
1716pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1719 row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1720 Value::Object(row)
1721}
1722
1723pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1725 rows.into_iter().map(|r| tag_row(r, t)).collect()
1726}
1727
1728fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1731 let Some((first, rest)) = path.split_first() else {
1732 return;
1733 };
1734 if rest.is_empty() {
1735 out.insert(*first, leaf);
1736 return;
1737 }
1738 let mut child = match out.get(first) {
1739 Some(Value::Object(o)) => o.clone(),
1740 _ => Object::new(),
1741 };
1742 set_nested(&mut child, rest, leaf);
1743 out.insert(*first, Value::Object(child));
1744}
1745
1746#[must_use]
1749pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1750 let mut out = String::with_capacity(glob.len() + 4);
1751 for ch in glob.chars() {
1752 match ch {
1753 '%' | '_' => {
1754 out.push('\\');
1755 out.push(ch);
1756 }
1757 '\\' if escape_backslash => out.push_str("\\\\"),
1758 '*' => out.push('%'),
1759 c => out.push(c),
1760 }
1761 }
1762 out
1763}
1764
1765impl DataContext for StoreContext<'_> {
1766 fn root(&self, name: &str) -> Value {
1767 if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1768 let mut slot = rr.rows.borrow_mut();
1769 let rows = if rr.once { slot.take() } else { slot.clone() };
1770 return Value::Array(rows.unwrap_or_default());
1771 }
1772 if name == "id" {
1776 return Value::Str(self.repo_id.clone());
1777 }
1778 Target::parse(name).map_or(Value::Undefined, scan_marker)
1781 }
1782
1783 fn root_object(&self) -> Value {
1786 self.root_row()
1787 }
1788
1789 fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1790 if row.is_absent() {
1791 return Ok(Value::Undefined);
1792 }
1793 if is_root_object(row) {
1796 if key == "id" {
1797 return Ok(Value::Str(self.repo_id.clone()));
1798 }
1799 return Ok(Target::parse(key).map_or(Value::Undefined, scan_marker));
1800 }
1801 let Some(t) = target_of(row) else {
1802 return Ok(oqx::DefaultContext::read(row, key));
1804 };
1805 if let Some(full) = self.resolve_lazy(row, t, key)? {
1808 return self.get(&full, key);
1809 }
1810 if key == "$self" {
1815 return Ok(row.clone());
1816 }
1817 if attributes(t).contains(&key) {
1819 return self.attribute(row, t, key);
1820 }
1821 match (t, key) {
1823 (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1824 return Ok(row.clone());
1825 }
1826 (Target::Revisions, "doc") => return self.revision_doc(row),
1829 (_, "doc") => return self.owning_doc(row),
1830 (Target::Nodes, "block") => return self.owning_block(row),
1831 (Target::Links, "dst") => return self.destination_doc(row),
1832 _ => {}
1833 }
1834 if let Some(v) = self.relation(row, t, key)? {
1835 return Ok(v);
1836 }
1837 if let Some(named) = Target::parse(key) {
1844 return Err(OqxError::eval(crate::query::bare_target_message(
1845 named, None,
1846 )));
1847 }
1848 Ok(match t {
1850 Target::Docs => {
1851 let doc_id = col_str(row, "doc_id");
1852 if SOURCE_HANDLES.contains(&key) {
1857 let source = (key != "data").then_some(key);
1858 return self.doc_prop_bag(&doc_id, source);
1859 }
1860 return self.doc_prop(&doc_id, key, None);
1861 }
1862 Target::Blocks | Target::Nodes => Self::jattr(row, key),
1866 Target::Links | Target::Revisions => Value::Undefined,
1870 })
1871 }
1872
1873 fn to_rows(&self, value: &Value) -> Vec<Value> {
1874 match value {
1875 Value::Undefined | Value::Null => Vec::new(),
1876 Value::Array(a) => a.clone(),
1877 other if is_absent_single(other) => Vec::new(),
1878 other => match scan_of(other) {
1879 Some(t) => self.expand_scan(t),
1880 None => vec![other.clone()],
1881 },
1882 }
1883 }
1884
1885 fn is_single(&self, value: &Value) -> bool {
1889 scan_of(value).is_none()
1890 }
1891
1892 fn materialize(&self, value: Value) -> Value {
1897 if value.as_object().is_some_and(|o| o.len() == 1) {
1898 if let Some(t) = scan_of(&value) {
1899 return Value::Array(self.expand_scan(t));
1900 }
1901 if is_absent_single(&value) {
1902 return Value::Undefined;
1903 }
1904 }
1905 value
1906 }
1907
1908 fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
1909 crate::store_index::index_for(self, scan_of(collection)?, path)
1910 }
1911
1912 fn identity(&self, row: &Value) -> Value {
1913 match target_of(row) {
1914 Some(Target::Docs) => col(row, "doc_id").clone(),
1915 Some(Target::Blocks) => col(row, "block_id").clone(),
1916 Some(Target::Nodes) => col(row, "node_id").clone(),
1917 Some(Target::Links) => col(row, "edge_id").clone(),
1918 Some(Target::Revisions) => col(row, "rev_id").clone(),
1919 None => row.clone(),
1920 }
1921 }
1922
1923 fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
1924 if name == "refs" {
1927 return Some(self.refs(args.first().unwrap_or(&Value::Undefined)));
1928 }
1929 if name == "range" {
1930 let x = args.first().unwrap_or(&Value::Undefined);
1931 return Some(Ok(match x {
1932 Value::Range(_) => x.clone(),
1933 Value::Str(s) => match omgbase_properties::detect_range(s) {
1934 Some(r) => {
1935 let b = |b: &Bound| match b {
1936 Bound::Open => Value::Undefined,
1937 Bound::Num(n) => Value::Number(*n),
1938 Bound::Iso(s) => Value::Str(s.clone()),
1939 };
1940 Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
1941 }
1942 None => Value::Null,
1943 },
1944 _ => Value::Null,
1945 }));
1946 }
1947 builtin_function(name, args)
1948 }
1949
1950 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1951 if let Some(t) = target_of(recv) {
1954 let full;
1956 let recv = if is_lazy_owner(recv) {
1957 let id_col = if t == Target::Docs {
1958 "doc_id"
1959 } else {
1960 "block_id"
1961 };
1962 match self.load_owner(t, &col_str(recv, id_col)) {
1963 Ok(Some(v)) => {
1964 full = v;
1965 &full
1966 }
1967 Ok(None) => recv,
1968 Err(e) => return Some(Err(e)),
1969 }
1970 } else {
1971 recv
1972 };
1973 if let Some(r) = self.row_method(name, recv, t, args) {
1974 return Some(r);
1975 }
1976 } else if matches!(
1977 name,
1978 "text"
1979 | "semantic"
1980 | "under"
1981 | "under_heading"
1982 | "within"
1983 | "under_kind"
1984 | "yaml_path"
1985 | "json_pointer"
1986 | "has_edge"
1987 | "has_anchor"
1988 | "child_count"
1989 | "parent_type"
1990 ) {
1991 return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/links row"));
1992 }
1993 if name == "matches" {
1994 if let Some(r) = self.matches_memoized(recv, args) {
1995 return Some(r);
1996 }
1997 }
1998 builtin_method_with(RegexDialect::Oqx, name, recv, args)
1999 }
2000}
2001
2002#[cfg(test)]
2003mod tests {
2004 use super::*;
2005
2006 #[test]
2007 fn glob_to_like_escapes() {
2008 assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
2009 assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
2010 assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
2011 }
2012
2013 #[test]
2014 fn rows_surfacing_as_values_render_id_and_path() {
2015 let mut node = Object::new();
2019 node.insert("node_id", Value::Str("n_1".into()));
2020 node.insert("attrs", Value::Str("{\"checked\":true}".into()));
2021 node.insert("__path", Value::Str("a.md".into()));
2022 let mut doc = Object::new();
2023 doc.insert("doc_id", Value::Str("d_0".into()));
2024 doc.insert("path", Value::Str("a.md".into()));
2025 doc.insert("blob", Value::Str("ff".into()));
2026 let mut record = Object::new();
2027 record.insert(TAG_KEY, Value::Str("junk".into()));
2028 record.insert(
2029 "tasks",
2030 Value::Array(vec![
2031 tag_row(node, Target::Nodes),
2032 tag_row(doc, Target::Docs),
2033 ]),
2034 );
2035 let out = render_row_values(Value::Object(record));
2036 let o = out.as_object().unwrap();
2037 assert!(o.get(TAG_KEY).is_none());
2038 let tasks = o.get("tasks").unwrap().as_array().unwrap();
2039 let keys = |v: &Value| -> Vec<String> {
2040 v.as_object()
2041 .unwrap()
2042 .iter()
2043 .map(|(k, _)| k.to_owned())
2044 .collect()
2045 };
2046 assert_eq!(keys(&tasks[0]), ["id", "path"]);
2047 assert_eq!(
2048 tasks[0].as_object().unwrap().get("id"),
2049 Some(&Value::Str("n_1".into()))
2050 );
2051 assert_eq!(
2052 tasks[0].as_object().unwrap().get("path"),
2053 Some(&Value::Str("/a.md".into()))
2054 );
2055 assert_eq!(keys(&tasks[1]), ["id", "path"]);
2056 assert_eq!(
2057 tasks[1].as_object().unwrap().get("id"),
2058 Some(&Value::Str("d_0".into()))
2059 );
2060 let mut edge = Object::new();
2062 edge.insert("edge_id", Value::Number(7.0));
2063 let e = render_row_values(tag_row(edge, Target::Links));
2064 assert_eq!(
2065 e.as_object().unwrap().get("id"),
2066 Some(&Value::Str("7".into()))
2067 );
2068 assert_eq!(
2069 e.as_object().unwrap().get("path"),
2070 Some(&Value::Str(String::new()))
2071 );
2072 }
2073
2074 #[test]
2075 fn nested_property_objects_rebuild() {
2076 let mut o = Object::new();
2077 set_nested(&mut o, &["a", "b"], Value::Number(1.0));
2078 set_nested(&mut o, &["a", "c"], Value::Number(2.0));
2079 set_nested(&mut o, &["d"], Value::Str("x".into()));
2080 let a = o.get("a").unwrap().as_object().unwrap();
2081 assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
2082 assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
2083 assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
2084 set_nested(&mut o, &["d", "e"], Value::Bool(true));
2086 assert!(o.get("d").unwrap().as_object().is_some());
2087 }
2088
2089 #[test]
2090 fn json_and_sql_bridges() {
2091 assert_eq!(
2092 parse_json(&Value::Str("{\"a\":1}".into()))
2093 .as_object()
2094 .unwrap()
2095 .get("a"),
2096 Some(&Value::Number(1.0))
2097 );
2098 assert_eq!(
2099 parse_json(&Value::Str("nope".into())),
2100 Value::Str("nope".into())
2101 );
2102 assert_eq!(parse_json(&Value::Null), Value::Undefined);
2103 assert_eq!(arg_or_empty(&[], 0), "");
2104 assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
2105 }
2106}