1use std::cell::RefCell;
39use std::collections::HashMap;
40
41use omgbase_properties::Bound;
42use omgbase_search::{cosine_bytes, sanitize_fts_query};
43use oqx::semantics::{builtin_function, builtin_method_with, make_range, string_form};
44use oqx::{CompiledRegex, DataContext, Object, OqxError, RegexDialect, Value, compile_regex};
45use rusqlite::types::{Value as SqlValue, ValueRef};
46use rusqlite::{Connection, OptionalExtension, params_from_iter};
47
48pub const TAG_KEY: &str = "__oqx_target";
50const REPO_TAG: &str = "$repo";
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum Target {
55 Docs,
56 Blocks,
57 Nodes,
58 Edges,
59}
60
61impl Target {
62 #[must_use]
64 pub fn as_str(self) -> &'static str {
65 match self {
66 Target::Docs => "docs",
67 Target::Blocks => "blocks",
68 Target::Nodes => "nodes",
69 Target::Edges => "edges",
70 }
71 }
72
73 #[must_use]
75 pub fn parse(s: &str) -> Option<Self> {
76 Some(match s {
77 "docs" => Target::Docs,
78 "blocks" => Target::Blocks,
79 "nodes" => Target::Nodes,
80 "edges" => Target::Edges,
81 _ => return None,
82 })
83 }
84}
85
86const RESERVED_DOC_BASENAMES: [&str; 5] = ["id", "path", "updated_at", "content_hash", "body"];
89
90#[derive(Clone, Debug, PartialEq)]
93pub struct SemanticVec {
94 pub model: String,
95 pub vec: Vec<u8>,
96}
97
98pub struct StoreContext<'a> {
100 conn: &'a Connection,
101 repo_id: String,
102 semantic: HashMap<String, SemanticVec>,
103 root_failure: RefCell<Option<OqxError>>,
106 rows_root: Option<RowsRoot>,
108 regexes: RefCell<HashMap<(String, Option<String>), CompiledRegex>>,
111}
112
113struct RowsRoot {
118 rows: RefCell<Option<Vec<Value>>>,
119 once: bool,
120}
121
122fn sql_value(v: ValueRef<'_>) -> Value {
123 match v {
124 ValueRef::Null => Value::Null,
125 ValueRef::Integer(i) => Value::Number(i as f64),
126 ValueRef::Real(f) => Value::Number(f),
127 ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
128 ValueRef::Blob(b) => Value::Str(omgbase_format::hash::hex(b)),
129 }
130}
131
132pub(crate) fn to_sql(v: &Value) -> SqlValue {
137 match v {
138 Value::Undefined | Value::Null | Value::Range(_) => SqlValue::Null,
139 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
140 Value::Number(n) => SqlValue::Real(*n),
141 Value::Str(s) => SqlValue::Text(s.clone()),
142 Value::Array(_) | Value::Object(_) => SqlValue::Text(v.to_string()),
143 }
144}
145
146fn js_string(v: &Value) -> String {
148 v.to_string()
149}
150
151fn arg_or_empty(args: &[Value], i: usize) -> String {
153 match args.get(i) {
154 None | Some(Value::Undefined) | Some(Value::Null) => String::new(),
155 Some(v) => js_string(v),
156 }
157}
158
159fn parse_json(v: &Value) -> Value {
161 match v {
162 Value::Str(s) => {
163 serde_json::from_str::<serde_json::Value>(s).map_or_else(|_| v.clone(), Value::from)
164 }
165 Value::Null | Value::Undefined => Value::Undefined,
166 other => other.clone(),
167 }
168}
169
170fn sql_err(e: rusqlite::Error) -> OqxError {
173 OqxError::eval(format!("sqlite: {e}"))
174}
175
176pub fn target_of(row: &Value) -> Option<Target> {
178 row.as_object()
179 .and_then(|o| o.get(TAG_KEY))
180 .and_then(Value::as_str)
181 .and_then(Target::parse)
182}
183
184fn is_repo_root(row: &Value) -> bool {
185 row.as_object()
186 .and_then(|o| o.get(TAG_KEY))
187 .and_then(Value::as_str)
188 == Some(REPO_TAG)
189}
190
191fn col<'v>(row: &'v Value, key: &str) -> &'v Value {
192 row.as_object()
193 .and_then(|o| o.get(key))
194 .unwrap_or(&Value::Undefined)
195}
196
197fn col_str(row: &Value, key: &str) -> String {
198 match col(row, key) {
199 Value::Undefined | Value::Null => String::new(),
200 v => js_string(v),
201 }
202}
203
204#[must_use]
206pub fn strip_tags(v: Value) -> Value {
207 match v {
208 Value::Object(o) => Value::Object(
209 o.into_iter()
210 .filter(|(k, _)| k != TAG_KEY)
211 .map(|(k, x)| (k, strip_tags(x)))
212 .collect(),
213 ),
214 Value::Array(a) => Value::Array(a.into_iter().map(strip_tags).collect()),
215 other => other,
216 }
217}
218
219#[must_use]
226pub fn render_row_values(v: Value) -> Value {
227 match v {
228 Value::Object(o) => {
229 let row = Value::Object(o);
230 if let Some(t) = target_of(&row) {
231 let (id_col, path_col) = match t {
232 Target::Docs => ("doc_id", "path"),
233 Target::Blocks => ("block_id", "__path"),
234 Target::Nodes => ("node_id", "__path"),
235 Target::Edges => ("edge_id", "__path"),
236 };
237 let mut out = Object::with_capacity(2);
238 out.insert("id", Value::Str(col_str(&row, id_col)));
239 out.insert("path", Value::Str(col_str(&row, path_col)));
240 return Value::Object(out);
241 }
242 let Value::Object(o) = row else {
243 unreachable!()
244 };
245 Value::Object(
246 o.into_iter()
247 .filter(|(k, _)| k != TAG_KEY)
248 .map(|(k, x)| (k, render_row_values(x)))
249 .collect(),
250 )
251 }
252 Value::Array(a) => Value::Array(a.into_iter().map(render_row_values).collect()),
253 other => other,
254 }
255}
256
257impl<'a> StoreContext<'a> {
258 #[must_use]
261 pub fn new(
262 conn: &'a Connection,
263 repo_id: &str,
264 semantic: HashMap<String, SemanticVec>,
265 ) -> Self {
266 Self {
267 conn,
268 repo_id: repo_id.to_owned(),
269 semantic,
270 root_failure: RefCell::new(None),
271 rows_root: None,
272 regexes: RefCell::new(HashMap::new()),
273 }
274 }
275
276 #[must_use]
281 pub fn with_rows_root(mut self, rows: Vec<Value>) -> Self {
282 self.rows_root = Some(RowsRoot {
283 rows: RefCell::new(Some(rows)),
284 once: false,
285 });
286 self
287 }
288
289 #[must_use]
297 pub fn with_rows_root_once(mut self, rows: Vec<Value>) -> Self {
298 self.rows_root = Some(RowsRoot {
299 rows: RefCell::new(Some(rows)),
300 once: true,
301 });
302 self
303 }
304
305 pub fn take_root_failure(&self) -> Option<OqxError> {
310 self.root_failure.borrow_mut().take()
311 }
312
313 fn all(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Vec<Object>> {
316 fetch_rows(self.conn, sql, params).map_err(sql_err)
317 }
318
319 fn one(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Option<Object>> {
320 Ok(self.all(sql, params)?.into_iter().next())
321 }
322
323 fn scalar(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<Value> {
324 Ok(self
325 .one(sql, params)?
326 .and_then(|o| o.values().next().cloned())
327 .unwrap_or(Value::Undefined))
328 }
329
330 fn exists(&self, sql: &str, params: &[SqlValue]) -> oqx::Result<bool> {
331 let mut stmt = self.conn.prepare_cached(sql).map_err(sql_err)?;
332 stmt.exists(params_from_iter(params.iter()))
333 .map_err(sql_err)
334 }
335
336 fn tag_all(rows: Vec<Object>, t: Target) -> Value {
337 Value::Array(tag_rows(rows, t))
338 }
339
340 fn tag(row: Object, t: Target) -> Value {
341 tag_row(row, t)
342 }
343
344 fn repo_root(&self) -> Value {
345 let mut o = Object::with_capacity(1);
346 o.insert(TAG_KEY, Value::Str(REPO_TAG.to_owned()));
347 Value::Object(o)
348 }
349
350 fn root_scan(&self, t: Target) -> oqx::Result<Value> {
363 let repo = [SqlValue::Text(self.repo_id.clone())];
364 let sql = match t {
365 Target::Docs => {
366 "SELECT * FROM docs WHERE repo_id = ?1 AND deleted_commit IS NULL ORDER BY path, doc_id"
367 }
368 Target::Blocks => {
369 "SELECT b.*, d.path AS __path FROM docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id
370 WHERE d.repo_id = ?1 AND +b.repo_id = ?1 AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
371 ORDER BY d.path, b.block_id"
372 }
373 Target::Nodes => {
374 "SELECT n.*, d.path AS __path FROM docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id
375 WHERE d.repo_id = ?1 AND +n.repo_id = ?1 AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id"
376 }
377 Target::Edges => {
378 "SELECT e.*, d.path AS __path FROM docs d CROSS JOIN edges e ON e.src_doc = d.doc_id
379 WHERE d.repo_id = ?1 AND +e.repo_id = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL
380 ORDER BY d.path, e.edge_id"
381 }
382 };
383 Ok(Self::tag_all(self.all(sql, &repo)?, t))
384 }
385
386 fn decode_prop(r: &Object) -> Value {
391 let get = |k: &str| r.get(k).cloned().unwrap_or(Value::Undefined);
392 match get("type").as_str().unwrap_or("") {
393 "number" => get("val_num"),
394 "bool" => Value::Bool(get("val_bool").truthy()),
395 "null" => Value::Null,
396 "json" => parse_json(&get("val_json")),
397 _ => get("val_text"),
398 }
399 }
400
401 fn doc_prop(&self, doc_id: &str, key: &str, source: Option<&str>) -> oqx::Result<Value> {
404 let rows = match source {
405 Some(s) => self.all(
406 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
407 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned()), SqlValue::Text(s.to_owned())],
408 )?,
409 None => self.all(
410 "SELECT * FROM properties WHERE doc_id = ?1 AND key = ?2 AND deleted_commit IS NULL ORDER BY ord",
411 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(key.to_owned())],
412 )?,
413 };
414 if rows.is_empty() {
415 return self.doc_prop_object(doc_id, key, source);
416 }
417 if rows.len() == 1 && rows[0].get("card").and_then(Value::as_str) == Some("scalar") {
418 return Ok(Self::decode_prop(&rows[0]));
419 }
420 Ok(Value::Array(rows.iter().map(Self::decode_prop).collect()))
421 }
422
423 fn doc_prop_object(
426 &self,
427 doc_id: &str,
428 prefix: &str,
429 source: Option<&str>,
430 ) -> oqx::Result<Value> {
431 let like = SqlValue::Text(format!("{prefix}.%"));
432 let rows = match source {
433 Some(s) => self.all(
434 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND source = ?3 AND deleted_commit IS NULL ORDER BY ord",
435 &[SqlValue::Text(doc_id.to_owned()), like, SqlValue::Text(s.to_owned())],
436 )?,
437 None => self.all(
438 "SELECT * FROM properties WHERE doc_id = ?1 AND key LIKE ?2 AND deleted_commit IS NULL ORDER BY ord",
439 &[SqlValue::Text(doc_id.to_owned()), like],
440 )?,
441 };
442 if rows.is_empty() {
443 return Ok(Value::Undefined);
444 }
445 let mut out = Object::new();
446 for r in &rows {
447 let key = r.get("key").and_then(Value::as_str).unwrap_or("");
448 let rest: Vec<&str> = key[(prefix.len() + 1).min(key.len())..]
449 .split('.')
450 .collect();
451 set_nested(&mut out, &rest, Self::decode_prop(r));
452 }
453 Ok(Value::Object(out))
454 }
455
456 fn doc_prop_bag(&self, doc_id: &str, source: &str) -> oqx::Result<Value> {
459 let keys = self.all(
460 "SELECT DISTINCT key FROM properties WHERE doc_id = ?1 AND source = ?2 AND deleted_commit IS NULL ORDER BY key",
461 &[SqlValue::Text(doc_id.to_owned()), SqlValue::Text(source.to_owned())],
462 )?;
463 let mut out = Object::new();
464 for k in keys {
465 let key = k.get("key").and_then(Value::as_str).unwrap_or("");
466 let top = key.split('.').next().unwrap_or("");
467 if !out.contains_key(top) {
468 let v = self.doc_prop(doc_id, top, Some(source))?;
469 out.insert(top, v);
470 }
471 }
472 Ok(Value::Object(out))
473 }
474
475 fn top_ordinal(&self, block: &Value) -> oqx::Result<Value> {
480 let ordinal = col(block, "ordinal").clone();
481 if col(block, "parent_block").is_absent() {
482 return Ok(ordinal);
483 }
484 let ap = col_str(block, "ancestor_path");
485 let Some(first) = ap.split('/').find(|s| !s.is_empty()) else {
486 return Ok(ordinal);
487 };
488 let r = self.scalar(
489 "SELECT ordinal FROM blocks WHERE doc_id = ?1 AND block_id = ?2",
490 &[
491 SqlValue::Text(col_str(block, "doc_id")),
492 SqlValue::Text(first.to_owned()),
493 ],
494 )?;
495 Ok(if r.is_absent() { ordinal } else { r })
496 }
497
498 fn doc_blocks_preorder(&self, doc_id: &str, path: &str) -> oqx::Result<Vec<Value>> {
502 let rows = self.all(
503 "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",
504 &[SqlValue::Text(path.to_owned()), SqlValue::Text(doc_id.to_owned())],
505 )?;
506 let ids: Vec<String> = rows
507 .iter()
508 .map(|r| {
509 r.get("block_id")
510 .and_then(Value::as_str)
511 .unwrap_or("")
512 .to_owned()
513 })
514 .collect();
515 let parent_index: Vec<Option<usize>> = rows
516 .iter()
517 .map(|r| {
518 r.get("parent_block")
519 .and_then(Value::as_str)
520 .and_then(|p| ids.iter().position(|id| id == p))
521 })
522 .collect();
523 let mut children: Vec<Vec<usize>> = vec![Vec::new(); rows.len()];
524 let mut roots = Vec::new();
525 for (i, p) in parent_index.iter().enumerate() {
526 match p {
527 Some(p) => children[*p].push(i),
528 None => roots.push(i),
529 }
530 }
531 fn walk(i: usize, children: &[Vec<usize>], order: &mut Vec<usize>) {
532 order.push(i);
533 for &c in &children[i] {
534 walk(c, children, order);
535 }
536 }
537 let mut order = Vec::with_capacity(rows.len());
538 for r in roots {
539 walk(r, &children, &mut order);
540 }
541 let mut slots: Vec<Option<Object>> = rows.into_iter().map(Some).collect();
542 Ok(order
543 .into_iter()
544 .map(|i| Self::tag(slots[i].take().expect("visited once"), Target::Blocks))
545 .collect())
546 }
547
548 fn jattr(row: &Value, k: &str) -> Value {
549 match parse_json(col(row, "attrs")) {
550 Value::Object(o) => o.get(k).cloned().unwrap_or(Value::Undefined),
551 _ => Value::Undefined,
552 }
553 }
554
555 fn relation(&self, row: &Value, t: Target, key: &str) -> oqx::Result<Option<Value>> {
557 let path = || SqlValue::Text(col_str(row, "__path"));
558 let doc_id = || SqlValue::Text(col_str(row, "doc_id"));
559 let doc_path = || SqlValue::Text(col_str(row, "path"));
560 let block_id = || SqlValue::Text(col_str(row, "block_id"));
561 let repo = || SqlValue::Text(self.repo_id.clone());
562 Ok(Some(match (t, key) {
563 (Target::Docs, "nodes") => {
564 let rows = self.all(
567 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 ORDER BY n.node_id",
568 &[doc_path(), doc_id()],
569 )?;
570 let blocks = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?;
571 let rank: HashMap<String, usize> = blocks
572 .iter()
573 .enumerate()
574 .map(|(i, b)| (col_str(b, "block_id"), i))
575 .collect();
576 let mut keyed: Vec<((usize, usize, f64, String), Object)> = rows
577 .into_iter()
578 .map(|r| {
579 let block = r.get("block_id").and_then(Value::as_str);
580 let (has_block, rk) = match block {
581 None => (0, 0),
582 Some(b) => (1, rank.get(b).copied().unwrap_or(usize::MAX)),
583 };
584 let span = r.get("span_start").and_then(Value::as_f64).unwrap_or(-1.0);
585 let id = r.get("node_id").and_then(Value::as_str).unwrap_or("").to_owned();
586 ((has_block, rk, span, id), r)
587 })
588 .collect();
589 keyed.sort_by(|a, b| {
590 a.0.0
591 .cmp(&b.0.0)
592 .then(a.0.1.cmp(&b.0.1))
593 .then(a.0.2.total_cmp(&b.0.2))
594 .then(a.0.3.cmp(&b.0.3))
595 });
596 Value::Array(keyed.into_iter().map(|(_, r)| Self::tag(r, Target::Nodes)).collect())
597 }
598 (Target::Docs, "blocks") => {
599 Value::Array(self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "path"))?)
600 }
601 (Target::Docs, "out") => Self::tag_all(
602 self.all(
603 "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.dst_node = d2.doc_id
604 WHERE e.src_doc = ?1 AND e.to_commit IS NULL AND d2.repo_id = ?2 AND d2.deleted_commit IS NULL ORDER BY d2.path, d2.doc_id",
605 &[doc_id(), repo()],
606 )?,
607 Target::Docs,
608 ),
609 (Target::Docs, "in") => Self::tag_all(
610 self.all(
611 "SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.src_doc = d2.doc_id
612 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d2.repo_id = ?2 AND d2.deleted_commit IS NULL ORDER BY d2.path, d2.doc_id",
613 &[doc_id(), repo()],
614 )?,
615 Target::Docs,
616 ),
617 (Target::Docs, "out_edges") => Self::tag_all(
618 self.all(
619 "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",
620 &[doc_path(), doc_id()],
621 )?,
622 Target::Edges,
623 ),
624 (Target::Docs, "in_edges") => Self::tag_all(
625 self.all(
626 "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
627 WHERE e.dst_node = ?1 AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id",
628 &[doc_id()],
629 )?,
630 Target::Edges,
631 ),
632 (Target::Blocks, "children") => Self::tag_all(
633 self.all(
634 "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",
635 &[path(), block_id()],
636 )?,
637 Target::Blocks,
638 ),
639 (Target::Blocks, "nodes") => Self::tag_all(
640 self.all(
641 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.block_id = ?2 ORDER BY n.span_start, n.node_id",
642 &[path(), block_id()],
643 )?,
644 Target::Nodes,
645 ),
646 (Target::Blocks, "out_edges") => Self::tag_all(
647 self.all(
648 "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",
649 &[path(), block_id()],
650 )?,
651 Target::Edges,
652 ),
653 (Target::Blocks, "section") => {
654 let top = to_sql(&self.top_ordinal(row)?);
655 Self::tag_all(
656 self.all(
657 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
658 AND json_extract(n.attrs,'$.first_ordinal') <= ?3 AND json_extract(n.attrs,'$.last_ordinal') >= ?4
659 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
660 &[path(), doc_id(), top.clone(), top],
661 )?,
662 Target::Nodes,
663 )
664 }
665 (Target::Nodes, "blocks") => {
666 let (f, l) = (Self::jattr(row, "first_ordinal"), Self::jattr(row, "last_ordinal"));
667 if f.is_absent() || l.is_absent() {
668 return Ok(Some(Value::Array(Vec::new())));
669 }
670 let (f, l) = (
671 f.as_f64().unwrap_or(f64::NAN),
672 l.as_f64().unwrap_or(f64::NAN),
673 );
674 let rows = self.doc_blocks_preorder(&col_str(row, "doc_id"), &col_str(row, "__path"))?;
675 let mut kept: Vec<Value> = Vec::new();
676 for b in rows {
677 let t = self.top_ordinal(&b)?.as_f64().unwrap_or(f64::NAN);
678 if t >= f && t <= l {
679 kept.push(b);
680 }
681 }
682 Value::Array(kept)
683 }
684 (Target::Nodes, "subsections") => {
685 let (f, l, lvl) = (
686 Self::jattr(row, "first_ordinal"),
687 Self::jattr(row, "last_ordinal"),
688 Self::jattr(row, "level"),
689 );
690 if f.is_absent() {
691 return Ok(Some(Value::Array(Vec::new())));
692 }
693 Self::tag_all(
694 self.all(
695 "SELECT n.*, ?1 AS __path FROM nodes n WHERE n.doc_id = ?2 AND n.kind = 'md:section'
696 AND json_extract(n.attrs,'$.first_ordinal') >= ?3 AND json_extract(n.attrs,'$.last_ordinal') <= ?4
697 AND json_extract(n.attrs,'$.level') > ?5 ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id",
698 &[path(), doc_id(), to_sql(&f), to_sql(&l), to_sql(&lvl)],
699 )?,
700 Target::Nodes,
701 )
702 }
703 (Target::Nodes, "children") => {
704 let (f, l, lvl) = (
705 Self::jattr(row, "first_ordinal"),
706 Self::jattr(row, "last_ordinal"),
707 Self::jattr(row, "level"),
708 );
709 if f.is_absent() {
710 return Ok(Some(Value::Array(Vec::new())));
711 }
712 Self::tag_all(
713 self.all(
714 "SELECT i.*, ?1 AS __path FROM nodes i WHERE i.doc_id = ?2 AND i.kind = 'md:section'
715 AND json_extract(i.attrs,'$.level') > ?3
716 AND json_extract(i.attrs,'$.first_ordinal') >= ?4 AND json_extract(i.attrs,'$.last_ordinal') <= ?5
717 AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
718 AND json_extract(m.attrs,'$.level') > ?6 AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
719 AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
720 AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
721 ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id",
722 &[path(), doc_id(), to_sql(&lvl), to_sql(&f), to_sql(&l), to_sql(&lvl)],
723 )?,
724 Target::Nodes,
725 )
726 }
727 _ => return Ok(None),
728 }))
729 }
730
731 fn owning_doc(&self, row: &Value) -> oqx::Result<Value> {
732 let id = match col(row, "doc_id") {
733 Value::Undefined | Value::Null => col(row, "src_doc").clone(),
734 v => v.clone(),
735 };
736 Ok(self
737 .one("SELECT * FROM docs WHERE doc_id = ?1", &[to_sql(&id)])?
738 .map_or(Value::Undefined, |o| Self::tag(o, Target::Docs)))
739 }
740
741 fn owning_block(&self, row: &Value) -> oqx::Result<Value> {
742 let id = col(row, "block_id");
743 if !id.truthy() {
744 return Ok(Value::Undefined);
745 }
746 Ok(self
747 .one(
748 "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1",
749 &[to_sql(id)],
750 )?
751 .map_or(Value::Undefined, |o| Self::tag(o, Target::Blocks)))
752 }
753
754 fn null_if_absent(v: Value) -> Value {
757 if v.is_absent() { Value::Null } else { v }
758 }
759
760 fn intrinsic(&self, row: &Value, t: Target, name: &str) -> oqx::Result<Value> {
761 if name == "$self" {
762 return Ok(row.clone());
763 }
764 let c = |k: &str| col(row, k).clone();
765 Ok(match (t, name) {
766 (Target::Docs, "$id") => c("doc_id"),
767 (Target::Docs, "$path") => c("path"),
768 (Target::Docs, "$content_hash") => Self::null_if_absent(c("file_hash")),
769 (Target::Docs, "$updated_at") => Self::null_if_absent(self.scalar(
770 "SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?1",
771 &[to_sql(&c("current_rev"))],
772 )?),
773 (Target::Docs, "$body") => {
774 match omgbase_store::read::reconstruct(self.conn, &col_str(row, "doc_id")) {
775 Ok(Some(s)) => Value::Str(s),
776 Ok(None) => Value::Null,
777 Err(e) => return Err(OqxError::eval(e.to_string())),
778 }
779 }
780 (Target::Docs, "$title") => {
781 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$title", Some("computed"))?)
782 }
783 (Target::Docs, "$tags") => {
784 Self::null_if_absent(self.doc_prop(&col_str(row, "doc_id"), "$tags", Some("computed"))?)
785 }
786 (Target::Blocks, "$id") => c("block_id"),
787 (Target::Blocks, "$doc") => c("doc_id"),
788 (Target::Blocks, "$path") => c("__path"),
789 (Target::Blocks, "$ordinal") => c("ordinal"),
790 (Target::Blocks, "$depth") => c("depth"),
791 (Target::Blocks, "$body") => c("text"),
792 (Target::Blocks, "$content_hash") => Self::null_if_absent(c("raw_hash")),
793 (Target::Blocks, "$updated_at") => Self::null_if_absent(self.scalar(
794 "SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?1",
795 &[to_sql(&c("block_id"))],
796 )?),
797 (Target::Nodes, "$id" | "$node_id") => c("node_id"),
798 (Target::Nodes, "$doc_id") => c("doc_id"),
799 (Target::Nodes, "$block_id") => c("block_id"),
800 (Target::Nodes, "$path") => c("__path"),
801 (Target::Edges, "$id") => c("edge_id"),
802 (Target::Edges, "$src") => c("src_doc"),
803 (Target::Edges, "$dst") => c("dst_node"),
804 (Target::Edges, "$src_block") => c("src_block"),
805 (Target::Edges, "$via") => c("via_node"),
806 (Target::Edges, "$from_commit") => c("from_commit"),
807 (Target::Edges, "$path") => c("__path"),
808 (Target::Edges, "$dst_path") => Self::null_if_absent(self.scalar(
809 "SELECT path FROM docs WHERE doc_id = ?1",
810 &[to_sql(&c("dst_node"))],
811 )?),
812 (Target::Edges, "$dst_uri") => Self::null_if_absent(self.scalar(
813 "SELECT uri FROM external_nodes WHERE node_id = ?1",
814 &[to_sql(&c("dst_node"))],
815 )?),
816 _ => Value::Undefined,
817 })
818 }
819
820 fn filter_invalid(msg: String) -> Option<oqx::Result<Value>> {
823 Some(Err(OqxError::eval(msg)))
824 }
825
826 fn require_target(t: Target, want: Target, name: &str) -> Option<oqx::Result<Value>> {
827 (t != want).then(|| {
828 Err(OqxError::eval(format!(
829 "{name}() is only available on the {} target",
830 want.as_str()
831 )))
832 })
833 }
834
835 fn sql_result(r: oqx::Result<bool>) -> oqx::Result<Value> {
836 r.map(Value::Bool)
837 }
838
839 fn row_method(
840 &self,
841 name: &str,
842 row: &Value,
843 t: Target,
844 args: &[Value],
845 ) -> Option<oqx::Result<Value>> {
846 let c = |k: &str| col(row, k).clone();
847 match name {
848 "text" => Some(self.text_match(t, row, &arg_or_empty(args, 0))),
849 "semantic" => Some(self.semantic_score(t, row, &arg_or_empty(args, 0))),
850 "has_anchor" => Self::require_target(t, Target::Blocks, name).or_else(|| {
851 Some(Self::sql_result(self.exists(
852 "SELECT 1 FROM edges WHERE src_block = ?1 AND anchor IS NOT NULL LIMIT 1",
853 &[to_sql(&c("block_id"))],
854 )))
855 }),
856 "child_count" => Self::require_target(t, Target::Blocks, name).or_else(|| {
857 Some(self.scalar(
858 "SELECT COUNT(*) FROM blocks WHERE parent_block = ?1 AND deleted_commit IS NULL",
859 &[to_sql(&c("block_id"))],
860 ))
861 }),
862 "parent_type" => Self::require_target(t, Target::Blocks, name).or_else(|| {
863 Some(
864 self.scalar(
865 "SELECT type FROM blocks WHERE block_id = ?1",
866 &[to_sql(&c("parent_block"))],
867 )
868 .map(Self::null_if_absent),
869 )
870 }),
871 "has_edge" => {
872 let pred = js_string(args.first().unwrap_or(&Value::Undefined));
873 let (src_col, src_val) = if t == Target::Blocks {
874 ("src_block", c("block_id"))
875 } else {
876 ("src_doc", c("doc_id"))
877 };
878 let r = if args.len() >= 2 {
879 self.exists(
880 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL AND dst_node = ?3 LIMIT 1"),
881 &[to_sql(&src_val), SqlValue::Text(pred), to_sql(&args[1])],
882 )
883 } else {
884 self.exists(
885 &format!("SELECT 1 FROM edges WHERE {src_col} = ?1 AND predicate = ?2 AND to_commit IS NULL LIMIT 1"),
886 &[to_sql(&src_val), SqlValue::Text(pred)],
887 )
888 };
889 Some(Self::sql_result(r))
890 }
891 "under" => Self::require_target(t, Target::Blocks, name).or_else(|| {
892 let target = js_string(args.first().unwrap_or(&Value::Undefined));
893 let ap = col_str(row, "ancestor_path");
894 Some(Ok(Value::Bool(
895 ap.contains(&format!("/{target}/")) || col_str(row, "block_id") == target,
896 )))
897 }),
898 "under_heading" => Self::require_target(t, Target::Blocks, name).or_else(|| {
899 let text = js_string(args.first().unwrap_or(&Value::Undefined));
900 let top = match self.top_ordinal(row) {
901 Ok(v) => to_sql(&v),
902 Err(e) => return Some(Err(e)),
903 };
904 Some(Self::sql_result(self.exists(
905 "SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
906 WHERE s.doc_id = ?1 AND lower(hb.text) LIKE '%' || lower(?2) || '%' AND s.first_ordinal <= ?3 AND s.last_ordinal >= ?4 LIMIT 1",
907 &[to_sql(&c("doc_id")), SqlValue::Text(text), top.clone(), top],
908 )))
909 }),
910 "within" => Self::require_target(t, Target::Blocks, name).or_else(|| {
911 let target = js_string(args.first().unwrap_or(&Value::Undefined));
912 if target.starts_with("d_") {
913 return Some(Ok(Value::Bool(col_str(row, "doc_id") == target)));
914 }
915 if target.contains('*') {
916 let like = glob_to_like(&target, false);
917 return Some(Self::sql_result(self.exists(
918 "SELECT 1 WHERE ?1 LIKE ?2 ESCAPE '\\'",
919 &[SqlValue::Text(col_str(row, "__path")), SqlValue::Text(like)],
920 )));
921 }
922 Some(Ok(Value::Bool(col_str(row, "__path") == target)))
923 }),
924 "under_kind" => Self::require_target(t, Target::Blocks, name).or_else(|| {
925 let kind = js_string(args.first().unwrap_or(&Value::Undefined));
926 let ap: Vec<String> = col_str(row, "ancestor_path")
927 .split('/')
928 .filter(|s| !s.is_empty())
929 .map(str::to_owned)
930 .collect();
931 if ap.is_empty() {
932 return Some(Ok(Value::Bool(false)));
933 }
934 let placeholders: Vec<String> = (1..=ap.len()).map(|i| format!("?{i}")).collect();
935 let placeholders = placeholders.join(",");
936 let mut params: Vec<SqlValue> = ap.into_iter().map(SqlValue::Text).collect();
937 let n = params.len();
938 params.push(SqlValue::Text(kind));
939 let r = match args.get(1) {
940 Some(v) if !v.is_absent() => {
941 let nm = js_string(v);
942 params.push(SqlValue::Text(nm.clone()));
943 params.push(SqlValue::Text(nm));
944 self.exists(
945 &format!(
946 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} AND (lower(text) LIKE '%' || lower(?{}) || '%' OR json_extract(attrs,'$.key') = ?{}) LIMIT 1",
947 n + 1,
948 n + 2,
949 n + 3
950 ),
951 ¶ms,
952 )
953 }
954 _ => self.exists(
955 &format!(
956 "SELECT 1 FROM blocks WHERE block_id IN ({placeholders}) AND type = ?{} LIMIT 1",
957 n + 1
958 ),
959 ¶ms,
960 ),
961 };
962 Some(Self::sql_result(r))
963 }),
964 "yaml_path" => Self::require_target(t, Target::Blocks, name).or_else(|| {
965 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "yaml")))
966 }),
967 "json_pointer" => Self::require_target(t, Target::Blocks, name).or_else(|| {
968 Some(Ok(Self::key_path(row, &js_string(args.first().unwrap_or(&Value::Undefined)), "json")))
969 }),
970 _ => None,
971 }
972 }
973
974 fn key_path(row: &Value, path: &str, kind: &str) -> Value {
975 let key = if kind == "json" {
976 let mut p = path;
977 p = p.strip_prefix('#').unwrap_or(p);
978 p = p.strip_prefix('/').unwrap_or(p);
979 p.split('/').collect::<Vec<_>>().join(".")
980 } else {
981 path.to_owned()
982 };
983 let leaf = key.rsplit('.').next().unwrap_or("").to_owned();
984 if !col_str(row, "type").starts_with(&format!("{kind}:")) {
985 return Value::Bool(false);
986 }
987 let k = Self::jattr(row, "key");
988 Value::Bool(k == Value::Str(leaf) || k == Value::Str(key))
989 }
990
991 fn text_match(&self, t: Target, row: &Value, terms: &str) -> oqx::Result<Value> {
992 if t == Target::Edges {
993 return Err(OqxError::eval(
994 "text(...) is not available on the edges target",
995 ));
996 }
997 let m = sanitize_fts_query(terms);
998 if m.is_empty() {
999 return Ok(Value::Bool(false));
1000 }
1001 let r = match t {
1002 Target::Docs => self.exists(
1003 "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",
1004 &[to_sql(col(row, "doc_id")), SqlValue::Text(m)],
1005 ),
1006 Target::Nodes => self.exists(
1007 "SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?1) AND nodes_fts MATCH ?2",
1008 &[to_sql(col(row, "node_id")), SqlValue::Text(m)],
1009 ),
1010 _ => self.exists(
1011 "SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?1) AND blocks_fts MATCH ?2",
1012 &[to_sql(col(row, "block_id")), SqlValue::Text(m)],
1013 ),
1014 };
1015 Self::sql_result(r)
1016 }
1017
1018 fn matches_memoized(&self, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1028 let flags = match args.get(1) {
1029 None | Some(Value::Undefined) | Some(Value::Null) => None,
1030 Some(Value::Str(s)) => Some(s.clone()),
1031 Some(_) => return None,
1032 };
1033 let Some(subject) = string_form(recv) else {
1034 return Some(Ok(Value::Bool(false)));
1035 };
1036 let pattern = args.first().unwrap_or(&Value::Undefined).to_string();
1037 let key = (pattern, flags);
1038 let mut memo = self.regexes.borrow_mut();
1039 if !memo.contains_key(&key) {
1040 let flags_value = args.get(1).cloned().unwrap_or(Value::Undefined);
1041 match compile_regex(&key.0, &flags_value, RegexDialect::Oqx) {
1042 Ok(re) => {
1043 memo.insert(key.clone(), re);
1044 }
1045 Err(_) => return None,
1046 }
1047 }
1048 Some(Ok(Value::Bool(memo[&key].is_match(&subject))))
1049 }
1050
1051 fn semantic_score(&self, t: Target, row: &Value, phrase: &str) -> oqx::Result<Value> {
1052 if matches!(t, Target::Nodes | Target::Edges) {
1053 return Err(OqxError::eval(
1054 "semantic(...) is available on the docs and blocks targets",
1055 ));
1056 }
1057 let Some(resolved) = self.semantic.get(phrase) else {
1058 return Err(OqxError::eval(format!(
1059 "semantic({}) needs an embedding provider; none is configured for this query",
1060 serde_json::Value::String(phrase.to_owned())
1061 )));
1062 };
1063 let vec: oqx::Result<Option<Vec<u8>>> = match t {
1064 Target::Docs => self
1065 .conn
1066 .query_row(
1067 "SELECT vec FROM doc_embeddings WHERE doc_id = ?1 AND model = ?2",
1068 rusqlite::params![col_str(row, "doc_id"), resolved.model],
1069 |r| r.get(0),
1070 )
1071 .optional()
1072 .map_err(sql_err),
1073 _ => omgbase_store::block_vector(self.conn, &col_str(row, "block_id"), &resolved.model)
1076 .map_err(|e| OqxError::eval(e.to_string())),
1077 };
1078 match vec? {
1079 Some(v) => Ok(Value::Number(cosine_bytes(&v, &resolved.vec))),
1080 None => Ok(Value::Null),
1081 }
1082 }
1083}
1084
1085pub(crate) fn fetch_rows(
1088 conn: &Connection,
1089 sql: &str,
1090 params: &[SqlValue],
1091) -> rusqlite::Result<Vec<Object>> {
1092 let mut stmt = conn.prepare_cached(sql)?;
1093 let names: Vec<String> = stmt
1094 .column_names()
1095 .iter()
1096 .map(|s| (*s).to_owned())
1097 .collect();
1098 let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
1099 let mut o = Object::with_capacity(names.len() + 1);
1102 for (i, name) in names.iter().enumerate() {
1103 o.insert(name.as_str(), sql_value(r.get_ref(i)?));
1104 }
1105 Ok(o)
1106 })?;
1107 rows.collect()
1108}
1109
1110pub(crate) fn tag_row(mut row: Object, t: Target) -> Value {
1113 row.insert(TAG_KEY, Value::Str(t.as_str().to_owned()));
1114 Value::Object(row)
1115}
1116
1117pub(crate) fn tag_rows(rows: Vec<Object>, t: Target) -> Vec<Value> {
1119 rows.into_iter().map(|r| tag_row(r, t)).collect()
1120}
1121
1122fn set_nested(out: &mut Object, path: &[&str], leaf: Value) {
1125 let Some((first, rest)) = path.split_first() else {
1126 return;
1127 };
1128 if rest.is_empty() {
1129 out.insert(*first, leaf);
1130 return;
1131 }
1132 let mut child = match out.get(first) {
1133 Some(Value::Object(o)) => o.clone(),
1134 _ => Object::new(),
1135 };
1136 set_nested(&mut child, rest, leaf);
1137 out.insert(*first, Value::Object(child));
1138}
1139
1140#[must_use]
1143pub fn glob_to_like(glob: &str, escape_backslash: bool) -> String {
1144 let mut out = String::with_capacity(glob.len() + 4);
1145 for ch in glob.chars() {
1146 match ch {
1147 '%' | '_' => {
1148 out.push('\\');
1149 out.push(ch);
1150 }
1151 '\\' if escape_backslash => out.push_str("\\\\"),
1152 '*' => out.push('%'),
1153 c => out.push(c),
1154 }
1155 }
1156 out
1157}
1158
1159impl DataContext for StoreContext<'_> {
1160 fn root(&self, name: &str) -> Value {
1161 if let Some(rr) = self.rows_root.as_ref().filter(|_| name == oqx::ROWS_ROOT) {
1162 let mut slot = rr.rows.borrow_mut();
1163 let rows = if rr.once { slot.take() } else { slot.clone() };
1164 return Value::Array(rows.unwrap_or_default());
1165 }
1166 if name == "$repo" {
1167 return self.repo_root();
1168 }
1169 let Some(t) = Target::parse(name) else {
1170 return Value::Undefined;
1171 };
1172 match self.root_scan(t) {
1175 Ok(rows) => rows,
1176 Err(e) => {
1177 let mut slot = self.root_failure.borrow_mut();
1178 if slot.is_none() {
1179 *slot = Some(e);
1180 }
1181 Value::Array(Vec::new())
1182 }
1183 }
1184 }
1185
1186 fn get(&self, row: &Value, key: &str) -> oqx::Result<Value> {
1187 if row.is_absent() {
1188 return Ok(Value::Undefined);
1189 }
1190 if key == "$repo" {
1193 return Ok(self.repo_root());
1194 }
1195 if is_repo_root(row) {
1196 if key == "$id" {
1197 return Ok(Value::Str(self.repo_id.clone()));
1198 }
1199 return match Target::parse(key) {
1200 Some(t) => self.root_scan(t),
1201 None => Ok(Value::Undefined),
1202 };
1203 }
1204 let Some(t) = target_of(row) else {
1205 return Ok(oqx::DefaultContext::read(row, key));
1207 };
1208 if key.starts_with('$') {
1209 return self.intrinsic(row, t, key);
1210 }
1211 match (t, key) {
1213 (Target::Docs, "doc") | (Target::Blocks, "block") | (Target::Nodes, "section") => {
1214 return Ok(row.clone());
1215 }
1216 (_, "doc") => return self.owning_doc(row),
1217 (Target::Nodes, "block") => return self.owning_block(row),
1218 _ => {}
1219 }
1220 if let Some(v) = self.relation(row, t, key)? {
1221 return Ok(v);
1222 }
1223 let c = |k: &str| col(row, k).clone();
1224 Ok(match t {
1225 Target::Docs => {
1226 if key == "format" {
1227 return Ok(c("format"));
1228 }
1229 let doc_id = col_str(row, "doc_id");
1230 if key == "frontmatter" || key == "inline" {
1231 return self.doc_prop_bag(&doc_id, key);
1232 }
1233 if RESERVED_DOC_BASENAMES.contains(&key) {
1234 return Err(OqxError::eval(format!(
1238 "bare '{key}' reads a frontmatter key; did you mean the intrinsic ${key}? (use frontmatter.{key} to force the property)"
1239 )));
1240 }
1241 return self.doc_prop(&doc_id, key, None);
1242 }
1243 Target::Blocks => match key {
1244 "type" => c("type"),
1245 "text" => c("text"),
1246 "attrs" => parse_json(&c("attrs")),
1247 _ => Self::jattr(row, key),
1248 },
1249 Target::Nodes => match key {
1250 "kind" => c("kind"),
1251 "name" => c("name"),
1252 "value" => c("value"),
1253 "attrs" => parse_json(&c("attrs")),
1254 _ => Self::jattr(row, key),
1255 },
1256 Target::Edges => match key {
1257 "predicate" | "provenance" | "dst_kind" | "anchor" | "src_field" => c(key),
1258 _ => Value::Undefined,
1259 },
1260 })
1261 }
1262
1263 fn to_rows(&self, value: &Value) -> Vec<Value> {
1264 match value {
1265 Value::Undefined | Value::Null => Vec::new(),
1266 Value::Array(a) => a.clone(),
1267 other => vec![other.clone()],
1268 }
1269 }
1270
1271 fn identity(&self, row: &Value) -> Value {
1272 match target_of(row) {
1273 Some(Target::Docs) => col(row, "doc_id").clone(),
1274 Some(Target::Blocks) => col(row, "block_id").clone(),
1275 Some(Target::Nodes) => col(row, "node_id").clone(),
1276 Some(Target::Edges) => col(row, "edge_id").clone(),
1277 None => row.clone(),
1278 }
1279 }
1280
1281 fn call_function(&self, name: &str, args: &[Value]) -> Option<oqx::Result<Value>> {
1282 if name == "range" {
1283 let x = args.first().unwrap_or(&Value::Undefined);
1284 return Some(Ok(match x {
1285 Value::Range(_) => x.clone(),
1286 Value::Str(s) => match omgbase_properties::detect_range(s) {
1287 Some(r) => {
1288 let b = |b: &Bound| match b {
1289 Bound::Open => Value::Undefined,
1290 Bound::Num(n) => Value::Number(*n),
1291 Bound::Iso(s) => Value::Str(s.clone()),
1292 };
1293 Value::from(make_range(b(&r.lo), b(&r.hi), r.exclusive_end))
1294 }
1295 None => Value::Null,
1296 },
1297 _ => Value::Null,
1298 }));
1299 }
1300 builtin_function(name, args)
1301 }
1302
1303 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<oqx::Result<Value>> {
1304 if let Some(t) = target_of(recv) {
1305 if let Some(r) = self.row_method(name, recv, t, args) {
1306 return Some(r);
1307 }
1308 } else if matches!(
1309 name,
1310 "text"
1311 | "semantic"
1312 | "under"
1313 | "under_heading"
1314 | "within"
1315 | "under_kind"
1316 | "yaml_path"
1317 | "json_pointer"
1318 | "has_edge"
1319 | "has_anchor"
1320 | "child_count"
1321 | "parent_type"
1322 ) {
1323 return Self::filter_invalid(format!("{name}() needs a docs/blocks/nodes/edges row"));
1324 }
1325 if name == "matches" {
1326 if let Some(r) = self.matches_memoized(recv, args) {
1327 return Some(r);
1328 }
1329 }
1330 builtin_method_with(RegexDialect::Oqx, name, recv, args)
1331 }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use super::*;
1337
1338 #[test]
1339 fn glob_to_like_escapes() {
1340 assert_eq!(glob_to_like("a*/b_%", true), "a%/b\\_\\%");
1341 assert_eq!(glob_to_like("a\\b*", true), "a\\\\b%");
1342 assert_eq!(glob_to_like("a\\b*", false), "a\\b%");
1343 }
1344
1345 #[test]
1346 fn rows_surfacing_as_values_render_id_and_path() {
1347 let mut node = Object::new();
1350 node.insert("node_id", Value::Str("n_1".into()));
1351 node.insert("attrs", Value::Str("{\"checked\":true}".into()));
1352 node.insert("__path", Value::Str("a.md".into()));
1353 let mut doc = Object::new();
1354 doc.insert("doc_id", Value::Str("d_0".into()));
1355 doc.insert("path", Value::Str("a.md".into()));
1356 doc.insert("blob", Value::Str("ff".into()));
1357 let mut record = Object::new();
1358 record.insert(TAG_KEY, Value::Str("junk".into()));
1359 record.insert(
1360 "tasks",
1361 Value::Array(vec![
1362 tag_row(node, Target::Nodes),
1363 tag_row(doc, Target::Docs),
1364 ]),
1365 );
1366 let out = render_row_values(Value::Object(record));
1367 let o = out.as_object().unwrap();
1368 assert!(o.get(TAG_KEY).is_none());
1369 let tasks = o.get("tasks").unwrap().as_array().unwrap();
1370 let keys = |v: &Value| -> Vec<String> {
1371 v.as_object()
1372 .unwrap()
1373 .iter()
1374 .map(|(k, _)| k.to_owned())
1375 .collect()
1376 };
1377 assert_eq!(keys(&tasks[0]), ["id", "path"]);
1378 assert_eq!(
1379 tasks[0].as_object().unwrap().get("id"),
1380 Some(&Value::Str("n_1".into()))
1381 );
1382 assert_eq!(
1383 tasks[0].as_object().unwrap().get("path"),
1384 Some(&Value::Str("a.md".into()))
1385 );
1386 assert_eq!(keys(&tasks[1]), ["id", "path"]);
1387 assert_eq!(
1388 tasks[1].as_object().unwrap().get("id"),
1389 Some(&Value::Str("d_0".into()))
1390 );
1391 let mut edge = Object::new();
1393 edge.insert("edge_id", Value::Number(7.0));
1394 let e = render_row_values(tag_row(edge, Target::Edges));
1395 assert_eq!(
1396 e.as_object().unwrap().get("id"),
1397 Some(&Value::Str("7".into()))
1398 );
1399 assert_eq!(
1400 e.as_object().unwrap().get("path"),
1401 Some(&Value::Str(String::new()))
1402 );
1403 }
1404
1405 #[test]
1406 fn nested_property_objects_rebuild() {
1407 let mut o = Object::new();
1408 set_nested(&mut o, &["a", "b"], Value::Number(1.0));
1409 set_nested(&mut o, &["a", "c"], Value::Number(2.0));
1410 set_nested(&mut o, &["d"], Value::Str("x".into()));
1411 let a = o.get("a").unwrap().as_object().unwrap();
1412 assert_eq!(a.get("b"), Some(&Value::Number(1.0)));
1413 assert_eq!(a.get("c"), Some(&Value::Number(2.0)));
1414 assert_eq!(o.get("d"), Some(&Value::Str("x".into())));
1415 set_nested(&mut o, &["d", "e"], Value::Bool(true));
1417 assert!(o.get("d").unwrap().as_object().is_some());
1418 }
1419
1420 #[test]
1421 fn json_and_sql_bridges() {
1422 assert_eq!(
1423 parse_json(&Value::Str("{\"a\":1}".into()))
1424 .as_object()
1425 .unwrap()
1426 .get("a"),
1427 Some(&Value::Number(1.0))
1428 );
1429 assert_eq!(
1430 parse_json(&Value::Str("nope".into())),
1431 Value::Str("nope".into())
1432 );
1433 assert_eq!(parse_json(&Value::Null), Value::Undefined);
1434 assert_eq!(arg_or_empty(&[], 0), "");
1435 assert_eq!(arg_or_empty(&[Value::Number(2.0)], 0), "2");
1436 }
1437}