1use std::collections::HashMap;
13
14use omgbase_search::{EmbeddingProvider, f32_to_blob};
15use omgbase_store::Store;
16use oqx::ast::{BinaryOp, Expr, Query, SelectItem};
17use oqx::walk::{Clause, Node, VisitContext, Visitor, transform, visit};
18use oqx::{Consumer, Engine, InMemoryEngine, Value, build, resolve_aliases};
19use serde_json::{Map, Value as Json};
20
21use crate::context::{SemanticVec, StoreContext, Target, render_row_values, target_of};
22use crate::cursor::{decode_path_cursor, encode_cursor};
23use crate::dates;
24use crate::error::{Result, SurfaceError};
25use crate::names::{UnknownNames, VocabularyCache, check_names};
26use crate::paths::reference_path;
27use crate::planner::{SqlitePlanner, root_target};
28
29pub const DEFAULT_LIMIT: usize = 50;
31
32const ROW_FNS: [&str; 12] = [
38 "text",
39 "semantic",
40 "under",
41 "under_heading",
42 "within",
43 "under_kind",
44 "yaml_path",
45 "json_pointer",
46 "has_edge",
47 "has_anchor",
48 "child_count",
49 "parent_type",
50];
51
52const ID_KEY: &str = "__oqx_id";
53const PATH_KEY: &str = "__oqx_path";
54pub(crate) const HIT_ID: &str = "id";
60pub(crate) const HIT_PATH: &str = "path";
61const SELF_KEY: &str = "__oqx_self";
71const VALUE_KEY: &str = "__oqx_value";
75
76#[derive(Clone, Copy, Default)]
78pub struct QueryOptions<'a> {
79 pub limit: Option<usize>,
81 pub cursor: Option<&'a str>,
83 pub provider: Option<&'a dyn EmbeddingProvider>,
86 pub in_memory: bool,
89 pub bare: bool,
97 pub unknown_names: Option<UnknownNames>,
105 pub vocabulary: Option<&'a VocabularyCache>,
108 pub now_ms: Option<i64>,
114}
115
116#[derive(Clone, Debug, PartialEq)]
118pub struct OqxResult {
119 pub hits: Vec<Json>,
120 pub truncated: bool,
121 pub cursor: Option<String>,
122 pub consumer: Consumer,
123 pub count: Option<f64>,
124 pub exists: Option<bool>,
125 pub none: Option<bool>,
126 pub values: Option<Vec<Json>>,
128 pub hints: Vec<String>,
134}
135
136impl OqxResult {
137 fn scalar(consumer: Consumer) -> Self {
138 Self {
139 hits: Vec::new(),
140 truncated: false,
141 cursor: None,
142 consumer,
143 count: None,
144 exists: None,
145 none: None,
146 values: None,
147 hints: Vec::new(),
148 }
149 }
150
151 #[must_use]
154 pub fn to_json(&self) -> Json {
155 let mut m = Map::new();
156 m.insert("hits".to_owned(), Json::Array(self.hits.clone()));
157 m.insert("truncated".to_owned(), Json::Bool(self.truncated));
158 m.insert(
159 "cursor".to_owned(),
160 self.cursor.clone().map_or(Json::Null, Json::String),
161 );
162 m.insert(
163 "consumer".to_owned(),
164 Json::String(self.consumer.as_str().to_owned()),
165 );
166 if let Some(n) = self.count {
167 m.insert("count".to_owned(), Value::Number(n).to_canonical_json());
168 }
169 if let Some(b) = self.exists {
170 m.insert("exists".to_owned(), Json::Bool(b));
171 }
172 if let Some(b) = self.none {
173 m.insert("none".to_owned(), Json::Bool(b));
174 }
175 if let Some(v) = &self.values {
176 m.insert("values".to_owned(), Json::Array(v.clone()));
177 }
178 if !self.hints.is_empty() {
179 m.insert(
180 "hints".to_owned(),
181 Json::Array(self.hints.iter().cloned().map(Json::String).collect()),
182 );
183 }
184 Json::Object(m)
185 }
186}
187
188const PATH_ATTRIBUTES: [&str; 2] = ["path", "dst_path"];
199const ROW_HANDLES: [&str; 4] = ["doc", "block", "dst", "$it"];
204
205fn is_path_read(e: &Expr) -> bool {
206 match e {
207 Expr::Ident { name, .. } | Expr::Outer { name, .. } => {
208 PATH_ATTRIBUTES.contains(&name.as_str())
209 }
210 Expr::Member { recv, name, .. } if PATH_ATTRIBUTES.contains(&name.as_str()) => {
211 matches!(&**recv, Expr::Ident { name, .. } | Expr::Outer { name, .. } if ROW_HANDLES.contains(&name.as_str()))
212 }
213 _ => false,
214 }
215}
216
217fn envelope_collision(items: &[SelectItem]) -> Option<SurfaceError> {
228 for item in items {
229 let name = match item {
230 SelectItem::Field { name, .. } | SelectItem::Collect { name, .. } => name.as_str(),
231 };
232 if name != HIT_ID && name != HIT_PATH {
233 continue;
234 }
235 if let SelectItem::Field { expr, .. } = item
236 && matches!(expr, Expr::Ident { name: n, .. } if n == name)
237 {
238 continue;
239 }
240 let hint = if name == HIT_PATH {
241 "p: data.path"
242 } else {
243 "i: data.id"
244 };
245 return Some(SurfaceError::filter_invalid(
246 format!("projection `{name}` collides with the hit envelope — alias it (`{hint}`)"),
247 "OQX",
248 ));
249 }
250 None
251}
252
253fn root_literal(e: Expr) -> Expr {
254 match e {
255 Expr::Lit {
256 value: Value::Str(s),
257 span,
258 } => Expr::Lit {
259 value: Value::Str(reference_path(&s)),
260 span,
261 },
262 other => other,
263 }
264}
265
266#[must_use]
268pub fn root_path_literals(e: Expr) -> Expr {
269 match e {
270 Expr::Binary {
271 op: op @ (BinaryOp::Eq | BinaryOp::Ne),
272 left,
273 right,
274 span,
275 } => {
276 if is_path_read(&left) {
277 Expr::Binary {
278 op,
279 left,
280 right: Box::new(root_literal(*right)),
281 span,
282 }
283 } else if is_path_read(&right) {
284 Expr::Binary {
285 op,
286 left: Box::new(root_literal(*left)),
287 right,
288 span,
289 }
290 } else {
291 Expr::Binary {
292 op,
293 left,
294 right,
295 span,
296 }
297 }
298 }
299 Expr::Call {
300 recv: Some(recv),
301 name,
302 mut args,
303 span,
304 } if name == "startsWith" && args.len() == 1 && is_path_read(&recv) => {
305 let arg = root_literal(args.remove(0));
306 Expr::Call {
307 recv: Some(recv),
308 name,
309 args: vec![arg],
310 span,
311 }
312 }
313 other => other,
314 }
315}
316
317#[must_use]
329pub fn fold_dates(e: Expr, now_ms: i64) -> Expr {
330 let Expr::Call {
331 recv, name, args, ..
332 } = &e
333 else {
334 return e;
335 };
336 let mut vals = Vec::with_capacity(args.len());
337 for a in args {
338 let Expr::Lit { value, .. } = a else { return e };
339 vals.push(value.clone());
340 }
341 let folded = match recv {
342 None => {
343 if dates::date_function_arity(name) != Some(vals.len()) {
344 return e;
345 }
346 dates::date_function(name, &vals, now_ms)
347 }
348 Some(r) => {
349 let Expr::Lit { value, .. } = &**r else {
350 return e;
351 };
352 if dates::date_method_arity(name) != Some(vals.len()) {
353 return e;
354 }
355 dates::date_method(name, value, &vals)
356 }
357 };
358 match folded {
359 Some(Ok(v)) => build::lit(v),
360 _ => e,
361 }
362}
363
364#[must_use]
369pub fn rewrite_query(q: &Query, now_ms: i64) -> Query {
370 transform(q, &mut |e, _| match e {
371 Expr::Call {
372 recv: None,
373 name,
374 args,
375 span,
376 } if ROW_FNS.contains(&name.as_str()) => Expr::Call {
377 recv: Some(Box::new(build::ident("$self"))),
378 name,
379 args,
380 span,
381 },
382 other => fold_dates(root_path_literals(other), now_ms),
383 })
384}
385
386pub const REPO_REMOVED_MESSAGE: &str = "`$repo` was removed in surface 2.0 — reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^id` / `0^id`";
390
391#[must_use]
395pub fn bare_target_message(t: Target, depth: Option<usize>) -> String {
396 let name = t.as_str();
397 let noun = t.noun();
398 let carets = "^".repeat(depth.unwrap_or(1));
399 let how = if depth.is_none() {
400 format!("; one caret per enclosing block, or the absolute `0^{name}`")
401 } else {
402 String::new()
403 };
404 format!(
405 "`{name}` inside a block reads a property of the current row, which has none — did you mean `{carets}{name}` (the repository's {noun}{how})?"
406 )
407}
408
409#[must_use]
415pub fn unknown_target_message(name: &str) -> String {
416 let was = if name == "edges" {
417 " (`edges` was renamed `links` in surface 3.0)"
418 } else {
419 ""
420 };
421 let roots: Vec<String> = crate::context::ALL_TARGETS
422 .iter()
423 .map(|t| format!("`{}`", t.as_str()))
424 .collect();
425 format!(
426 "`{name}` is not a target — the repository's collections are {}{was}",
427 roots.join(", ")
428 )
429}
430
431#[must_use]
435pub fn past_root_message(t: Target, levels: usize) -> String {
436 let name = t.as_str();
437 let noun = t.noun();
438 let carets = "^".repeat(levels);
439 format!(
440 "`{carets}{name}` reaches past the root — there is no enclosing row at this depth; at the top level the repository's {noun} are the bare `{name}` (`{name} count {{ … }}`, `from {name}`, `entries({name})`)"
441 )
442}
443
444pub fn check_root_spellings(q: &Query) -> Result<()> {
458 if let Expr::Ident { name, .. } = &q.source {
462 if Target::parse(name).is_none() {
463 return Err(SurfaceError::filter_invalid(
464 unknown_target_message(name),
465 "OQX",
466 ));
467 }
468 }
469 struct Check(Option<SurfaceError>);
470 impl Visitor<'_> for Check {
471 fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
472 if self.0.is_some() {
473 return false;
474 }
475 match node {
476 Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. })
477 if name == "$repo" =>
478 {
479 self.0 = Some(SurfaceError::filter_invalid(REPO_REMOVED_MESSAGE, "OQX"));
480 }
481 Node::Expr(Expr::Ident { name, .. })
482 if ctx.depth >= 1 && matches!(name.as_str(), "docs" | "links") =>
483 {
484 let t = Target::parse(name).expect("a target name");
485 self.0 = Some(SurfaceError::filter_invalid(
486 bare_target_message(t, Some(ctx.depth)),
487 "OQX",
488 ));
489 }
490 Node::Expr(Expr::Outer { name, levels, .. }) if *levels > ctx.depth => {
491 if let Some(t) = Target::parse(name) {
492 self.0 = Some(SurfaceError::filter_invalid(
493 past_root_message(t, *levels),
494 "OQX",
495 ));
496 }
497 }
498 _ => {}
499 }
500 self.0.is_none()
501 }
502 }
503 let mut check = Check(None);
504 visit(Node::Query(q), &mut check);
505 check.0.map_or(Ok(()), Err)
506}
507
508struct Phrases(Vec<String>);
512
513impl Visitor<'_> for Phrases {
514 fn enter(&mut self, node: Node<'_>, _ctx: &VisitContext<'_>) -> bool {
515 if let Node::Expr(Expr::Call {
516 recv: None,
517 name,
518 args,
519 ..
520 }) = node
521 && name == "semantic"
522 && let Some(Expr::Lit {
523 value: Value::Str(s),
524 ..
525 }) = args.first()
526 && !self.0.contains(s)
527 {
528 self.0.push(s.clone());
529 }
530 true
531 }
532}
533
534#[must_use]
537pub fn collect_semantic_phrases(source: &str) -> Vec<String> {
538 let Ok(q) = oqx::parse_string(source) else {
539 return Vec::new();
540 };
541 semantic_phrases_of(&q)
542}
543
544#[must_use]
546pub fn semantic_phrases_of(q: &Query) -> Vec<String> {
547 let mut phrases = Phrases(Vec::new());
548 visit(Node::Query(q), &mut phrases);
549 phrases.0
550}
551
552pub fn embed_phrases(
559 provider: Option<&dyn EmbeddingProvider>,
560 phrases: Vec<String>,
561) -> Result<HashMap<String, SemanticVec>> {
562 let mut semantic: HashMap<String, SemanticVec> = HashMap::new();
563 if let Some(provider) = provider.filter(|_| !phrases.is_empty()) {
564 for phrase in phrases {
565 let vec = provider
566 .embed_query(&phrase)
567 .map_err(|e| SurfaceError::new(e.code(), e.to_string()))?;
568 semantic.insert(
569 phrase,
570 SemanticVec {
571 model: provider.model().to_owned(),
572 vec: f32_to_blob(&vec),
573 },
574 );
575 }
576 }
577 Ok(semantic)
578}
579
580pub const RESERVED_ALIAS_PREFIXES: [&str; 2] = ["__oqx_", "__omg_"];
591
592pub fn check_reserved_aliases(q: &Query) -> Result<()> {
594 for item in &q.select {
595 if let Some(prefix) = RESERVED_ALIAS_PREFIXES
596 .iter()
597 .find(|p| item.name().starts_with(*p))
598 {
599 return Err(SurfaceError::filter_invalid(
600 format!(
601 "projection `{}` uses the reserved prefix `{prefix}` — choose another alias",
602 item.name()
603 ),
604 "OQX",
605 ));
606 }
607 }
608 Ok(())
609}
610
611fn mentions_outside_source(q: &Query, name: &str) -> bool {
617 struct Mentions<'n> {
618 name: &'n str,
619 found: bool,
620 }
621 impl Visitor<'_> for Mentions<'_> {
622 fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
623 if self.found {
624 return false;
625 }
626 if ctx.clause == Some(Clause::Source) && ctx.path.len() == 1 {
628 return false;
629 }
630 if let Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. }) = node
631 && name == self.name
632 {
633 self.found = true;
634 }
635 !self.found
636 }
637 }
638 let mut m = Mentions { name, found: false };
639 visit(Node::Query(q), &mut m);
640 m.found
641}
642
643fn may_reach_non_rows(q: &Query) -> bool {
651 q.follow.is_some() || !q.from.is_empty() || root_target(&q.source).is_none()
652}
653
654fn describe_value(v: &Value) -> String {
656 match v {
657 Value::Undefined | Value::Null => "an absent value".to_owned(),
658 Value::Str(s) => format!(
659 "a string ({})",
660 serde_json::to_string(s).unwrap_or_default()
661 ),
662 Value::Number(_) => format!("a number ({v})"),
663 Value::Bool(b) => format!("a boolean ({b})"),
664 Value::Array(_) => "an array".to_owned(),
665 Value::Range(_) => "a range".to_owned(),
666 Value::Object(_) => "an object".to_owned(),
667 }
668}
669
670fn not_a_store_row(v: &Value) -> SurfaceError {
671 SurfaceError::filter_invalid(
672 format!(
673 "a hit must be a document, block, node, link or revision row — the query reached {}; to follow document references held in a property use refs(<field>)",
674 describe_value(v)
675 ),
676 "OQX",
677 )
678}
679
680#[derive(Debug)]
690struct Shaped {
691 id: String,
692 path: String,
693 proj: oqx::Object,
694}
695
696fn path_string(path: &Value) -> String {
697 if path.is_absent() {
698 String::new()
699 } else {
700 path.to_string()
701 }
702}
703
704fn shape_row(row: Value) -> Result<Shaped> {
705 let mut shaped = Shaped {
706 id: Value::Undefined.to_string(),
707 path: String::new(),
708 proj: oqx::Object::new(),
709 };
710 let Value::Object(mut o) = row else {
711 return Ok(shaped);
712 };
713 if let Some(me) = o.remove(SELF_KEY)
714 && target_of(&me).is_none()
715 {
716 return Err(not_a_store_row(&me));
717 }
718 let Value::Object(o) = render_row_values(Value::Object(o)) else {
719 return Ok(shaped);
720 };
721 for (k, v) in o {
722 match k.as_str() {
723 ID_KEY => shaped.id = v.to_string(),
724 PATH_KEY => shaped.path = path_string(&v),
725 _ => {
726 shaped.proj.insert(k, v);
727 }
728 }
729 }
730 Ok(shaped)
731}
732
733fn raw_identity(row: &Value) -> (String, String) {
738 let get = |k: &str| {
739 row.as_object()
740 .and_then(|o| o.get(k))
741 .cloned()
742 .unwrap_or(Value::Undefined)
743 };
744 (path_string(&get(PATH_KEY)), get(ID_KEY).to_string())
745}
746
747fn to_hit(s: Shaped) -> Value {
752 let mut hit = oqx::Object::with_capacity(s.proj.len() + 2);
753 hit.insert(HIT_ID, Value::Str(s.id));
754 hit.insert(HIT_PATH, Value::Str(s.path));
755 for (k, v) in s.proj {
756 hit.insert(k, v);
757 }
758 Value::Object(hit)
759}
760
761fn to_bare_hit(s: Shaped) -> Value {
766 Value::Object(s.proj)
767}
768
769fn render_hit(s: Shaped, bare: bool) -> Json {
771 if bare {
772 to_bare_hit(s).to_canonical_json()
773 } else {
774 to_hit(s).to_canonical_json()
775 }
776}
777
778fn dedup_by_projection(rows: Vec<Shaped>, bare: bool) -> Vec<Shaped> {
787 let mut seen: Vec<String> = Vec::new();
788 let mut out = Vec::new();
789 for s in rows {
790 let mut pairs: Vec<(String, Value)> = s
791 .proj
792 .iter()
793 .filter(|(k, _)| bare || (*k != HIT_ID && *k != HIT_PATH))
794 .map(|(k, v)| (k.to_owned(), v.clone()))
795 .collect();
796 pairs.sort_by(|a, b| a.0.cmp(&b.0));
797 let key = Value::Array(
798 pairs
799 .into_iter()
800 .map(|(k, v)| Value::Array(vec![Value::Str(k), v]))
801 .collect(),
802 )
803 .to_canonical_json()
804 .to_string();
805 if seen.contains(&key) {
806 continue;
807 }
808 seen.push(key);
809 out.push(s);
810 }
811 out
812}
813
814fn const_bound(e: Option<&Expr>, word: &str) -> Result<Option<usize>> {
817 match e {
818 None => Ok(None),
819 Some(Expr::Lit {
820 value: Value::Number(n),
821 ..
822 }) if n.fract() == 0.0 && *n >= 0.0 && n.is_finite() => Ok(Some(*n as usize)),
823 Some(_) => Err(SurfaceError::filter_invalid(
824 format!("top-level {word} must be a non-negative integer literal"),
825 "OQX",
826 )),
827 }
828}
829
830fn page_rows<T>(
836 mut rows: Vec<T>,
837 ident: impl Fn(&T) -> (String, String),
838 offset: usize,
839 limit: Option<usize>,
840 cursor: Option<&str>,
841 cap: usize,
842 custom: bool,
843) -> Result<(Vec<T>, bool, Option<String>)> {
844 if offset > 0 || limit.is_some() {
845 let end = limit.map_or(rows.len(), |l| (offset + l).min(rows.len()));
846 if offset >= rows.len() {
847 rows.clear();
848 } else {
849 rows.truncate(end);
850 rows.drain(..offset);
851 }
852 }
853 if let Some(cursor) = cursor {
854 let parts = decode_path_cursor(cursor, "query", 2)?;
856 let (path, id) = (&parts[0], &parts[1]);
857 rows.retain(|r| {
858 let (rp, ri) = ident(r);
859 rp > *path || (rp == *path && ri > *id)
860 });
861 }
862 let truncated = rows.len() > cap;
863 rows.truncate(cap);
864 let next = if truncated && !custom {
865 rows.last().map(|last| {
866 let (p, i) = ident(last);
867 encode_cursor(&[&p, &i])
868 })
869 } else {
870 None
871 };
872 Ok((rows, truncated, next))
873}
874
875fn value_of(s: &Shaped) -> Json {
876 s.proj
877 .get(VALUE_KEY)
878 .cloned()
879 .unwrap_or(Value::Undefined)
880 .to_canonical_json()
881}
882
883pub fn check_bare(q: &Query) -> Result<()> {
890 if q.values {
891 return Err(SurfaceError::filter_invalid(
892 BARE_WITH_VALUES_MESSAGE,
893 "OQX",
894 ));
895 }
896 if q.select.is_empty() {
897 return Err(SurfaceError::filter_invalid(
898 BARE_NEEDS_SELECT_MESSAGE,
899 "OQX",
900 ));
901 }
902 Ok(())
903}
904
905pub const BARE_NEEDS_SELECT_MESSAGE: &str =
907 "`bare` needs a `select` — without a projection a hit is only its envelope";
908pub const BARE_WITH_VALUES_MESSAGE: &str =
909 "`bare` is redundant with `values` — `values` is already the bare-values form; drop one";
910
911pub fn query(
915 store: &Store,
916 repo_id: &str,
917 source: &str,
918 opts: QueryOptions<'_>,
919) -> Result<OqxResult> {
920 let semantic = embed_phrases(opts.provider, collect_semantic_phrases(source))?;
921 let runner = Runner {
922 store,
923 repo_id,
924 semantic,
925 planned: !opts.in_memory,
926 now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
927 };
928 let raw = oqx::parse_string(source)?;
929 check_reserved_aliases(&raw)?;
930 run_parsed(
932 &runner,
933 &raw,
934 opts,
935 opts.unknown_names.unwrap_or(UnknownNames::Hint),
936 )
937}
938
939pub fn query_ast(
948 store: &Store,
949 repo_id: &str,
950 raw: &Query,
951 opts: QueryOptions<'_>,
952 semantic: HashMap<String, SemanticVec>,
953) -> Result<OqxResult> {
954 let runner = Runner {
955 store,
956 repo_id,
957 semantic,
958 planned: !opts.in_memory,
959 now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
960 };
961 run_parsed(
962 &runner,
963 raw,
964 opts,
965 opts.unknown_names.unwrap_or(UnknownNames::Refuse),
966 )
967}
968
969struct Runner<'a> {
975 store: &'a Store,
976 repo_id: &'a str,
977 semantic: HashMap<String, SemanticVec>,
978 planned: bool,
979 now_ms: i64,
981}
982
983impl Runner<'_> {
984 fn run(&self, q: &Query) -> Result<oqx::OqxResult> {
991 let conn = self.store.conn();
992 let ctx =
993 StoreContext::new(conn, self.repo_id, self.semantic.clone()).with_now_ms(self.now_ms);
994 let plan = if self.planned {
995 SqlitePlanner::new(conn, self.repo_id)
996 .try_plan(q, &[])
997 .map_err(|e| SurfaceError::other(format!("sqlite: {e}")))?
998 } else {
999 None
1000 };
1001 let (ctx, residual) = match plan {
1002 Some(plan) => {
1007 let once = !mentions_outside_source(&plan.residual, oqx::ROWS_ROOT);
1008 let ctx = if once {
1009 ctx.with_rows_root_once(plan.rows)
1010 } else {
1011 ctx.with_rows_root(plan.rows)
1012 };
1013 (ctx, Some(plan.residual))
1014 }
1015 None => (ctx, None),
1016 };
1017 let engine = InMemoryEngine::new(ctx);
1018 let out = engine.run(residual.as_ref().unwrap_or(q), &[]);
1019 if let Some(failed) = engine.context().take_root_failure() {
1022 return Err(failed.into());
1023 }
1024 Ok(out?)
1027 }
1028}
1029
1030fn run_parsed(
1031 engine: &Runner<'_>,
1032 raw: &Query,
1033 opts: QueryOptions<'_>,
1034 mode: UnknownNames,
1035) -> Result<OqxResult> {
1036 check_root_spellings(raw)?;
1037 let own = Query {
1045 select: raw
1046 .select
1047 .iter()
1048 .filter(|it| {
1049 !RESERVED_ALIAS_PREFIXES
1050 .iter()
1051 .any(|p| it.name().starts_with(p))
1052 })
1053 .cloned()
1054 .collect(),
1055 ..raw.clone()
1056 };
1057 let local = VocabularyCache::default();
1058 let cache = opts.vocabulary.unwrap_or(&local);
1059 let hints = check_names(engine.store, engine.repo_id, &own, mode, cache)?;
1060 let mut result = run_resolved(engine, raw, opts)?;
1061 result.hints = hints;
1063 Ok(result)
1064}
1065
1066fn run_resolved(engine: &Runner<'_>, raw: &Query, opts: QueryOptions<'_>) -> Result<OqxResult> {
1067 let parsed = rewrite_query(&resolve_aliases(raw)?, engine.now_ms);
1070 let consumer = parsed.consumer;
1071
1072 match consumer {
1073 Consumer::Exists => {
1074 let res = engine.run(&parsed)?;
1075 let mut r = OqxResult::scalar(consumer);
1076 r.exists = Some(matches!(res, oqx::OqxResult::Exists(true)));
1077 return Ok(r);
1078 }
1079 Consumer::Count => {
1080 let res = engine.run(&parsed)?;
1081 let mut r = OqxResult::scalar(consumer);
1082 r.count = Some(match res {
1083 oqx::OqxResult::Count(n) => n,
1084 _ => 0.0,
1085 });
1086 return Ok(r);
1087 }
1088 Consumer::None => {
1089 let res = engine.run(&parsed)?;
1090 let mut r = OqxResult::scalar(consumer);
1091 r.none = Some(match res {
1092 oqx::OqxResult::None(b) => b,
1093 _ => true,
1094 });
1095 return Ok(r);
1096 }
1097 Consumer::Collect | Consumer::First | Consumer::Single => {}
1098 }
1099
1100 if parsed.group_by.is_some() {
1112 return run_grouped(engine, &parsed, consumer, opts);
1113 }
1114
1115 let top_distinct = parsed.distinct;
1121 let top_values = parsed.values;
1122 let user_select: Vec<SelectItem> = if top_values {
1123 parsed
1124 .select
1125 .first()
1126 .map(|it| match it {
1127 SelectItem::Field {
1128 expr, lift, span, ..
1129 } => SelectItem::Field {
1130 name: VALUE_KEY.to_owned(),
1131 expr: expr.clone(),
1132 lift: *lift,
1133 span: *span,
1134 },
1135 SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1136 name: VALUE_KEY.to_owned(),
1137 op: op.clone(),
1138 span: *span,
1139 },
1140 })
1141 .into_iter()
1142 .collect()
1143 } else {
1144 parsed.select.clone()
1145 };
1146 let bare = opts.bare;
1150 if bare {
1151 check_bare(&parsed)?;
1152 } else if !top_values && let Some(e) = envelope_collision(&user_select) {
1153 return Err(e);
1154 }
1155 let id_item = build::field(ID_KEY, build::ident("id"));
1156 let path_item = build::field(PATH_KEY, build::ident("path"));
1157 let mut select = vec![id_item, path_item];
1158 if !top_values && may_reach_non_rows(&parsed) {
1159 select.push(build::field(SELF_KEY, build::ident("$it")));
1160 }
1161 select.extend(user_select);
1162 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1166 let q = Query {
1167 distinct: false,
1168 values: false,
1169 select,
1170 limit: if consumer == Consumer::Collect {
1171 None
1172 } else {
1173 parsed.limit.clone()
1174 },
1175 offset: if consumer == Consumer::Collect {
1176 None
1177 } else {
1178 parsed.offset.clone()
1179 },
1180 ..parsed.clone()
1181 };
1182 let res = engine.run(&q)?;
1183
1184 if matches!(consumer, Consumer::First | Consumer::Single) {
1185 let row = match res {
1186 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1187 _ => None,
1188 };
1189 let mut out = OqxResult::scalar(consumer);
1190 match row {
1191 None => {
1192 if top_values {
1193 out.values = Some(Vec::new());
1194 }
1195 }
1196 Some(r) => {
1197 let shaped = shape_row(r)?;
1198 if top_values {
1199 out.values = Some(vec![value_of(&shaped)]);
1200 } else {
1201 out.hits = vec![render_hit(shaped, bare)];
1202 }
1203 }
1204 }
1205 return Ok(out);
1206 }
1207
1208 let rows: Vec<Value> = match res {
1210 oqx::OqxResult::Collect(rows) => rows,
1211 _ => Vec::new(),
1212 };
1213 let custom = parsed.order_by.as_ref().is_some_and(|o| !o.is_empty());
1214 let cursor = opts.cursor.filter(|c| !c.is_empty() && !custom);
1215 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1216 let limit = const_bound(top_limit.as_ref(), "limit")?;
1217 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1218 let (page, truncated, next) = if top_distinct {
1224 let shaped = rows
1225 .into_iter()
1226 .map(shape_row)
1227 .collect::<Result<Vec<_>>>()?;
1228 let shaped = dedup_by_projection(shaped, bare);
1229 page_rows(
1230 shaped,
1231 |s| (s.path.clone(), s.id.clone()),
1232 offset,
1233 limit,
1234 cursor,
1235 cap,
1236 custom,
1237 )?
1238 } else {
1239 let (page, truncated, next) =
1240 page_rows(rows, raw_identity, offset, limit, cursor, cap, custom)?;
1241 let page = page
1242 .into_iter()
1243 .map(shape_row)
1244 .collect::<Result<Vec<_>>>()?;
1245 (page, truncated, next)
1246 };
1247 let mut out = OqxResult::scalar(Consumer::Collect);
1248 out.truncated = truncated;
1249 out.cursor = next;
1250 if top_values {
1251 out.values = Some(page.iter().map(value_of).collect());
1252 } else {
1253 out.hits = page.into_iter().map(|s| render_hit(s, bare)).collect();
1254 }
1255 Ok(out)
1256}
1257
1258fn run_grouped(
1260 engine: &Runner<'_>,
1261 parsed: &Query,
1262 consumer: Consumer,
1263 opts: QueryOptions<'_>,
1264) -> Result<OqxResult> {
1265 let top_values = parsed.values;
1266 let select: Vec<SelectItem> = if top_values {
1269 parsed
1270 .select
1271 .first()
1272 .map(|it| match it {
1273 SelectItem::Field {
1274 expr, lift, span, ..
1275 } => SelectItem::Field {
1276 name: VALUE_KEY.to_owned(),
1277 expr: expr.clone(),
1278 lift: *lift,
1279 span: *span,
1280 },
1281 SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1282 name: VALUE_KEY.to_owned(),
1283 op: op.clone(),
1284 span: *span,
1285 },
1286 })
1287 .into_iter()
1288 .collect()
1289 } else {
1290 parsed.select.clone()
1291 };
1292 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1293 let q = Query {
1294 values: false,
1295 select,
1296 limit: if consumer == Consumer::Collect {
1297 None
1298 } else {
1299 parsed.limit.clone()
1300 },
1301 offset: if consumer == Consumer::Collect {
1302 None
1303 } else {
1304 parsed.offset.clone()
1305 },
1306 ..parsed.clone()
1307 };
1308 let res = engine.run(&q)?;
1309 let project = |row: Value| -> oqx::Object {
1310 match render_row_values(row) {
1311 Value::Object(o) => o,
1312 _ => oqx::Object::new(),
1313 }
1314 };
1315 let value_of = |g: &oqx::Object| -> Json {
1316 g.get(VALUE_KEY)
1317 .cloned()
1318 .unwrap_or(Value::Undefined)
1319 .to_canonical_json()
1320 };
1321 if matches!(consumer, Consumer::First | Consumer::Single) {
1322 let row = match res {
1323 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1324 _ => None,
1325 };
1326 let mut out = OqxResult::scalar(consumer);
1327 match row {
1328 None => {
1329 if top_values {
1330 out.values = Some(Vec::new());
1331 }
1332 }
1333 Some(r) => {
1334 let g = project(r);
1335 if top_values {
1336 out.values = Some(vec![value_of(&g)]);
1337 } else {
1338 out.hits = vec![Value::Object(g).to_canonical_json()];
1339 }
1340 }
1341 }
1342 return Ok(out);
1343 }
1344 let mut groups: Vec<oqx::Object> = match res {
1345 oqx::OqxResult::Collect(rows) => rows.into_iter().map(project).collect(),
1346 _ => Vec::new(),
1347 };
1348 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1349 let limit = const_bound(top_limit.as_ref(), "limit")?;
1350 if offset > 0 || limit.is_some() {
1351 let end = limit.map_or(groups.len(), |l| (offset + l).min(groups.len()));
1352 if offset >= groups.len() {
1353 groups.clear();
1354 } else {
1355 groups.truncate(end);
1356 groups.drain(..offset);
1357 }
1358 }
1359 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1360 let truncated = groups.len() > cap;
1361 groups.truncate(cap);
1362 let mut out = OqxResult::scalar(Consumer::Collect);
1363 out.truncated = truncated;
1364 if top_values {
1365 out.values = Some(groups.iter().map(value_of).collect());
1366 } else {
1367 out.hits = groups
1368 .into_iter()
1369 .map(|g| Value::Object(g).to_canonical_json())
1370 .collect();
1371 }
1372 Ok(out)
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377 use super::*;
1378 use oqx::ast::Where;
1379
1380 #[test]
1381 fn row_functions_become_self_methods() {
1382 let q = oqx::parse_string(
1383 "from blocks where text(\"x\") && under_heading(\"h\") && size(attrs) > 0 && doc.path.startsWith(\"a\")",
1384 )
1385 .unwrap();
1386 let r = rewrite_query(&q, 0);
1387 let Some(Where::And { parts, .. }) = &r.r#where else {
1388 panic!("and")
1389 };
1390 let Where::Scalar { expr, .. } = &parts[0] else {
1391 panic!("scalar")
1392 };
1393 assert!(
1394 matches!(expr, Expr::Call { recv: Some(r), name, .. } if name == "text" && **r == build::ident("$self"))
1395 );
1396 let Where::Scalar { expr, .. } = &parts[2] else {
1397 panic!("scalar")
1398 };
1399 assert!(
1400 matches!(expr, Expr::Binary { left, .. } if matches!(&**left, Expr::Call { recv: None, name, .. } if name == "size"))
1401 );
1402 }
1403
1404 const NOW: i64 = 1_790_467_200_000;
1408
1409 fn where_expr(src: &str) -> Expr {
1410 let q = oqx::parse_string(&format!("from docs where {src}")).unwrap();
1411 let Some(Where::Scalar { expr, .. }) = q.r#where else {
1412 panic!("not a scalar where")
1413 };
1414 expr
1415 }
1416
1417 fn fold(src: &str) -> Expr {
1419 fn go(e: Expr) -> Expr {
1420 match e {
1421 Expr::Call {
1422 recv,
1423 name,
1424 args,
1425 span,
1426 } => fold_dates(
1427 Expr::Call {
1428 recv: recv.map(|r| Box::new(go(*r))),
1429 name,
1430 args: args.into_iter().map(go).collect(),
1431 span,
1432 },
1433 NOW,
1434 ),
1435 other => other,
1436 }
1437 }
1438 go(where_expr(src))
1439 }
1440
1441 fn lit(e: &Expr) -> Option<&Value> {
1442 match e {
1443 Expr::Lit { value, .. } => Some(value),
1444 _ => None,
1445 }
1446 }
1447
1448 #[test]
1449 fn folds_date_dur_over_literals_and_date_methods_over_literal_receivers_and_arguments() {
1450 assert_eq!(
1451 lit(&fold("date(\"2026-01-05\")")),
1452 Some(&Value::from("2026-01-05"))
1453 );
1454 assert_eq!(
1455 lit(&fold("date(\"today\")")),
1456 Some(&Value::from("2026-09-27"))
1457 );
1458 assert_eq!(
1459 lit(&fold("dur(\"30 days\")")),
1460 Some(&Value::from(30.0 * 86400.0))
1461 );
1462 assert_eq!(
1463 lit(&fold("date(\"today\").minus(dur(\"30 days\"))")),
1464 Some(&Value::from("2026-08-28"))
1465 );
1466 assert_eq!(lit(&fold("date(\"x\")")), Some(&Value::Null));
1467 assert_eq!(lit(&fold("date(5)")), Some(&Value::Null));
1468 assert_eq!(lit(&fold("dur(60).dur_text()")), Some(&Value::from("1m")));
1469 }
1470
1471 #[test]
1472 fn leaves_a_non_literal_argument_an_unknown_name_and_a_wrong_arity_in_place() {
1473 for src in [
1474 "date(when)",
1475 "date(\"today\").until(when)",
1476 "size(\"x\")",
1477 "date()",
1478 "date(\"2026-01-01\").plus()",
1479 "\"x\".lower()",
1480 ] {
1481 assert!(matches!(fold(src), Expr::Call { .. }), "{src}");
1482 }
1483 }
1484
1485 #[test]
1486 fn the_runner_folds_at_its_clock_so_a_named_point_pushes_down() {
1487 let q = oqx::parse_string("from docs where updated_at < date(\"today\") && date(when) < date(\"today\").minus(dur(\"30 days\"))").unwrap();
1488 let printed = oqx::print_query(&rewrite_query(&q, NOW)).unwrap();
1489 assert!(printed.contains("updated_at < \"2026-09-27\""), "{printed}");
1490 assert!(printed.contains("date(when) < \"2026-08-28\""), "{printed}");
1491 }
1492
1493 #[test]
1494 fn path_literals_are_rooted_only_against_path_reads() {
1495 let q = oqx::parse_string(
1496 "select n: ^docs collect { path where path == ^after || \"x.md\" != dst_path } from docs where path.startsWith(\"lab/\") && doc.path == \"a.md\" && type == \"a.md\" && ^path == \"b.md\"",
1497 )
1498 .unwrap();
1499 let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1500 assert!(printed.contains("path.startsWith(\"/lab/\")"), "{printed}");
1501 assert!(printed.contains("doc.path == \"/a.md\""), "{printed}");
1502 assert!(printed.contains("type == \"a.md\""), "{printed}");
1503 assert!(printed.contains("^path == \"/b.md\""), "{printed}");
1504 assert!(printed.contains("\"/x.md\" != dst_path"), "{printed}");
1505 assert!(printed.contains("path == ^after"), "{printed}");
1507 let q = oqx::parse_string(
1509 "from docs where path.contains(\"a/\") && path.endsWith(\".md\") && path == \"\"",
1510 )
1511 .unwrap();
1512 let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1513 assert!(printed.contains("path.contains(\"a/\")"), "{printed}");
1514 assert!(printed.contains("path.endsWith(\".md\")"), "{printed}");
1515 assert!(printed.contains("path == \"/\""), "{printed}");
1516 }
1517
1518 #[test]
1519 fn repo_and_bare_targets_are_refused_statically() {
1520 let parse = |s: &str| oqx::parse_string(s).unwrap();
1521 for q in [
1522 "from docs where $repo",
1523 "select r: size($repo.docs) from docs",
1524 "select r: $repo.id from docs",
1525 "select r: nodes first { select x: ^$repo.id values } from docs",
1526 "select r: nodes first { select x: 0^$repo values } from docs",
1527 "$repo.docs count { }",
1528 ] {
1529 let e = check_root_spellings(&parse(q)).unwrap_err();
1530 assert_eq!(e.code, "filter_invalid", "{q}");
1531 assert_eq!(e.message, REPO_REMOVED_MESSAGE, "{q}");
1532 }
1533 let e = check_root_spellings(&parse("select x: docs collect { } from docs")).unwrap_err();
1535 assert_eq!(e.message, bare_target_message(Target::Docs, Some(1)));
1536 assert!(
1537 e.message
1538 .contains("did you mean `^docs` (the repository's documents)?")
1539 );
1540 let e = check_root_spellings(&parse(
1541 "select x: nodes collect { select y: size(links) } from docs",
1542 ))
1543 .unwrap_err();
1544 assert!(
1545 e.message
1546 .contains("did you mean `^^links` (the repository's links)?")
1547 );
1548 let e = check_root_spellings(&parse("docs collect { from docs }")).unwrap_err();
1549 assert!(e.message.contains("`^docs`"));
1550 for q in [
1552 "docs count { }",
1553 "from docs where nodes exists { }",
1554 "entries(docs) first { }",
1555 "select n: size(^docs), id: ^id, k: size(0^links) from docs",
1556 "from blocks where blocks exists { }", ] {
1558 check_root_spellings(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1559 }
1560 let e = check_root_spellings(&parse("^docs count { }")).unwrap_err();
1562 assert_eq!(e.message, past_root_message(Target::Docs, 1));
1563 assert_eq!(
1564 e.message,
1565 "`^docs` reaches past the root — there is no enclosing row at this depth; at the top level the repository's documents are the bare `docs` (`docs count { … }`, `from docs`, `entries(docs)`)"
1566 );
1567 let e = check_root_spellings(&parse("select path from docs where ^^links exists { }"))
1568 .unwrap_err();
1569 assert!(
1570 e.message.contains("`^^links` reaches past the root"),
1571 "{}",
1572 e.message
1573 );
1574 let m = bare_target_message(Target::Blocks, None);
1576 assert!(m.contains("did you mean `^blocks` (the repository's blocks; one caret per enclosing block, or the absolute `0^blocks`)?"), "{m}");
1577 }
1578
1579 #[test]
1580 fn reserved_projection_names_are_refused() {
1581 let parse = |s: &str| oqx::parse_string(s).unwrap();
1582 let e = check_reserved_aliases(&parse("select __oqx_id: 1 from docs")).unwrap_err();
1583 assert_eq!(e.code, "filter_invalid");
1584 assert_eq!(
1585 e.message,
1586 "projection `__oqx_id` uses the reserved prefix `__oqx_` — choose another alias"
1587 );
1588 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1589 let e = check_reserved_aliases(&parse("select __omg_sel_0_x: 1 from docs")).unwrap_err();
1590 assert!(
1591 e.message.contains("reserved prefix `__omg_`"),
1592 "{}",
1593 e.message
1594 );
1595 assert!(
1597 check_reserved_aliases(&parse(
1598 "select __oqx: 1, x: nodes collect { select __oqx_y: 1 } from docs"
1599 ))
1600 .is_ok()
1601 );
1602 }
1603
1604 #[test]
1605 fn semantic_phrases_are_collected_distinct() {
1606 let phrases = collect_semantic_phrases(
1607 "select s: semantic(\"alpha\") from docs where semantic(\"alpha\") > 0.5 || nodes exists { where semantic(\"beta\") > 0 } order by semantic(\"gamma\") desc",
1608 );
1609 assert_eq!(phrases, ["alpha", "beta", "gamma"]);
1610 assert!(collect_semantic_phrases("not a query {{").is_empty());
1611 assert!(collect_semantic_phrases("from docs").is_empty());
1612 }
1613
1614 #[test]
1615 fn hits_peel_the_injected_columns() {
1616 let mut o = oqx::Object::new();
1617 o.insert(ID_KEY, Value::Str("d_1".into()));
1618 o.insert(PATH_KEY, Value::Null);
1619 o.insert("layer", Value::Str("canon".into()));
1620 let shaped = shape_row(Value::Object(o)).unwrap();
1621 assert_eq!((shaped.id.as_str(), shaped.path.as_str()), ("d_1", ""));
1622 let hit = to_hit(shaped);
1623 let ho = hit.as_object().unwrap();
1624 assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path", "layer"]);
1625 assert_eq!(ho.get("path"), Some(&Value::Str(String::new())));
1626 let mut o = oqx::Object::new();
1629 o.insert(ID_KEY, Value::Str("d_1".into()));
1630 o.insert(PATH_KEY, Value::Str("a.md".into()));
1631 o.insert("id", Value::Number(7.0));
1632 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1633 let ho = hit.as_object().unwrap();
1634 assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path"]);
1635 assert_eq!(ho.get("id"), Some(&Value::Number(7.0)));
1636 let mut o = oqx::Object::new();
1638 o.insert(ID_KEY, Value::Str("d_1".into()));
1639 o.insert(PATH_KEY, Value::Undefined);
1640 let row = Value::Object(o);
1641 assert_eq!(raw_identity(&row), (String::new(), "d_1".to_owned()));
1642 let shaped = shape_row(row).unwrap();
1643 assert_eq!((shaped.path, shaped.id), (String::new(), "d_1".to_owned()));
1644 }
1645
1646 #[test]
1647 fn bare_hits_are_the_projection_in_its_own_order() {
1648 let mut o = oqx::Object::new();
1651 o.insert(ID_KEY, Value::Str("d_1".into()));
1652 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1653 o.insert("title", Value::Str("T".into()));
1654 o.insert("id", Value::Str("d_1".into()));
1655 let shaped = shape_row(Value::Object(o)).unwrap();
1656 let bare = to_bare_hit(shaped);
1657 let bo = bare.as_object().unwrap();
1658 assert_eq!(bo.keys().collect::<Vec<_>>(), ["title", "id"]);
1659 let mut o = oqx::Object::new();
1662 o.insert(ID_KEY, Value::Str("d_1".into()));
1663 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1664 o.insert("path", Value::Str("canon".into()));
1665 let shaped = shape_row(Value::Object(o)).unwrap();
1666 assert_eq!(shaped.path, "/a.md");
1667 assert_eq!(
1668 render_hit(shaped, true),
1669 serde_json::json!({ "path": "canon" })
1670 );
1671 let mut o = oqx::Object::new();
1673 o.insert(ID_KEY, Value::Str("d_1".into()));
1674 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1675 o.insert("path", Value::Str("/a.md".into()));
1676 assert_eq!(
1677 render_hit(shape_row(Value::Object(o)).unwrap(), false),
1678 serde_json::json!({ "id": "d_1", "path": "/a.md" })
1679 );
1680 }
1681
1682 #[test]
1683 fn bare_needs_a_select_and_is_not_the_values_form() {
1684 let parse = |s: &str| oqx::parse_string(s).unwrap();
1685 for q in ["from docs", "from docs where layer == \"canon\""] {
1686 let e = check_bare(&parse(q)).unwrap_err();
1687 assert_eq!(e.code, "filter_invalid", "{q}");
1688 assert_eq!(e.message, BARE_NEEDS_SELECT_MESSAGE, "{q}");
1689 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1690 }
1691 let e = check_bare(&parse("select path values from docs")).unwrap_err();
1692 assert_eq!(e.code, "filter_invalid");
1693 assert_eq!(e.message, BARE_WITH_VALUES_MESSAGE);
1694 for q in [
1695 "select path from docs",
1696 "select id, title from docs",
1697 "docs first { select t: title }",
1698 ] {
1699 check_bare(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1700 }
1701 }
1702
1703 #[test]
1704 fn a_projection_onto_the_envelope_must_be_aliased() {
1705 let items = |src: &str| oqx::parse_string(src).unwrap().select;
1706 assert!(envelope_collision(&items("select id, path from docs")).is_none());
1708 assert!(
1709 envelope_collision(&items("select id: id, path: path, p: path from docs")).is_none()
1710 );
1711 let e = envelope_collision(&items("select data.path from docs")).unwrap();
1713 assert_eq!(e.code, "filter_invalid");
1714 assert_eq!(
1715 e.message,
1716 "projection `path` collides with the hit envelope — alias it (`p: data.path`)"
1717 );
1718 let e = envelope_collision(&items("select id: layer from docs")).unwrap();
1719 assert_eq!(
1720 e.message,
1721 "projection `id` collides with the hit envelope — alias it (`i: data.id`)"
1722 );
1723 assert!(envelope_collision(&items("select path: doc.path from blocks")).is_some());
1724 assert!(envelope_collision(&items("select path: nodes collect { } from docs")).is_some());
1725 }
1726
1727 #[test]
1728 fn a_path_read_is_the_attribute_or_a_row_handle_never_a_source_handle() {
1729 let pred = |src: &str| match oqx::parse_string(src).unwrap().r#where.unwrap() {
1730 oqx::ast::Where::Scalar { expr, .. } => expr,
1731 other => panic!("{other:?}"),
1732 };
1733 let left = |e: Expr| match e {
1734 Expr::Binary { left, .. } => *left,
1735 other => panic!("{other:?}"),
1736 };
1737 assert!(is_path_read(&left(pred("from docs where path == \"x\""))));
1738 assert!(is_path_read(&left(pred(
1739 "from blocks where doc.path == \"x\""
1740 ))));
1741 assert!(is_path_read(&left(pred("from docs where ^path == \"x\""))));
1742 assert!(is_path_read(&left(pred(
1743 "from links where dst_path == \"x\""
1744 ))));
1745 assert!(!is_path_read(&left(pred(
1746 "from docs where data.path == \"x\""
1747 ))));
1748 assert!(!is_path_read(&left(pred(
1749 "from docs where frontmatter.path == \"x\""
1750 ))));
1751 }
1752
1753 #[test]
1754 fn a_hit_that_is_not_a_store_row_fails_the_query() {
1755 let mut o = oqx::Object::new();
1758 o.insert(ID_KEY, Value::Undefined);
1759 o.insert(PATH_KEY, Value::Undefined);
1760 o.insert(SELF_KEY, Value::Str("/timeline/kickoff.md".into()));
1761 let e = shape_row(Value::Object(o)).unwrap_err();
1762 assert_eq!(e.code, "filter_invalid");
1763 assert_eq!(
1764 e.message,
1765 "a hit must be a document, block, node, link or revision row — the query reached a string (\"/timeline/kickoff.md\"); to follow document references held in a property use refs(<field>)"
1766 );
1767 let mut row = oqx::Object::new();
1769 row.insert("doc_id", Value::Str("d_1".into()));
1770 row.insert("path", Value::Str("a.md".into()));
1771 let row = crate::context::tag_row(row, crate::context::Target::Docs);
1772 let mut o = oqx::Object::new();
1773 o.insert(ID_KEY, Value::Str("d_1".into()));
1774 o.insert(PATH_KEY, Value::Str("a.md".into()));
1775 o.insert(SELF_KEY, row);
1776 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1777 assert_eq!(
1778 hit.as_object().unwrap().keys().collect::<Vec<_>>(),
1779 ["id", "path"]
1780 );
1781 let parse = |s: &str| oqx::parse_string(s).unwrap();
1783 assert!(!may_reach_non_rows(&parse(
1784 "from docs where layer == \"canon\""
1785 )));
1786 assert!(may_reach_non_rows(&parse("from docs follow before")));
1787 assert!(may_reach_non_rows(&parse("refs(\"/index.md\") first { }")));
1788 }
1789
1790 #[test]
1791 fn distinct_dedups_by_user_projection_first_wins() {
1792 let mk = |id: &str, t: &str| {
1793 let mut proj = oqx::Object::new();
1794 proj.insert("type", Value::Str(t.into()));
1795 Shaped {
1796 id: id.to_owned(),
1797 path: "p".to_owned(),
1798 proj,
1799 }
1800 };
1801 let out = dedup_by_projection(vec![mk("1", "a"), mk("2", "b"), mk("3", "a")], false);
1802 assert_eq!(out.len(), 2);
1803 assert_eq!(out[0].id, "1");
1804 assert_eq!(out[1].id, "2");
1805 let with_id = |id: &str| {
1809 let mut proj = oqx::Object::new();
1810 proj.insert("id", Value::Str(id.into()));
1811 Shaped {
1812 id: id.to_owned(),
1813 path: "p".to_owned(),
1814 proj,
1815 }
1816 };
1817 assert_eq!(
1818 dedup_by_projection(vec![with_id("1"), with_id("2")], false).len(),
1819 1
1820 );
1821 assert_eq!(
1822 dedup_by_projection(vec![with_id("1"), with_id("2")], true).len(),
1823 2
1824 );
1825 }
1826
1827 #[test]
1828 fn the_page_runs_on_identity() {
1829 let rows: Vec<(String, String)> = [("/a.md", "d_1"), ("/a.md", "d_2"), ("/b.md", "d_3")]
1830 .iter()
1831 .map(|(p, i)| ((*p).to_owned(), (*i).to_owned()))
1832 .collect();
1833 let ident = |r: &(String, String)| (r.0.clone(), r.1.clone());
1834 let (page, truncated, next) =
1836 page_rows(rows.clone(), ident, 0, None, None, 2, false).unwrap();
1837 assert_eq!(page.len(), 2);
1838 assert!(truncated);
1839 let next = next.expect("a cursor");
1840 assert_eq!(next, encode_cursor(&["/a.md", "d_2"]));
1841 let (page, truncated, next) =
1843 page_rows(rows.clone(), ident, 0, None, Some(&next), 2, false).unwrap();
1844 assert_eq!(page, vec![("/b.md".to_owned(), "d_3".to_owned())]);
1845 assert!(!truncated && next.is_none());
1846 let (page, truncated, next) =
1848 page_rows(rows.clone(), ident, 1, Some(1), None, 50, true).unwrap();
1849 assert_eq!(page, vec![("/a.md".to_owned(), "d_2".to_owned())]);
1850 assert!(!truncated && next.is_none());
1851 let (page, truncated, next) = page_rows(rows, ident, 0, None, None, 1, true).unwrap();
1852 assert_eq!(page.len(), 1);
1853 assert!(truncated && next.is_none());
1854 }
1855
1856 #[test]
1857 fn top_level_bounds_must_be_literals() {
1858 assert_eq!(const_bound(None, "limit").unwrap(), None);
1859 assert_eq!(
1860 const_bound(
1861 Some(&Expr::Lit {
1862 value: Value::Number(3.0),
1863 span: oqx::Span::EMPTY
1864 }),
1865 "limit"
1866 )
1867 .unwrap(),
1868 Some(3)
1869 );
1870 let e = const_bound(
1871 Some(&Expr::Lit {
1872 value: Value::Number(-1.0),
1873 span: oqx::Span::EMPTY,
1874 }),
1875 "offset",
1876 )
1877 .unwrap_err();
1878 assert_eq!(e.code, "filter_invalid");
1879 assert!(e.message.contains("top-level offset"));
1880 }
1881}