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