1use std::collections::HashMap;
13
14use omgbase_search::{EmbeddingProvider, f32_to_blob};
15use omgbase_store::Store;
16use oqx::ast::{BinaryOp, Expr, Flatten, 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 flatten_collision(flatten: &[Flatten]) -> Option<SurfaceError> {
258 let f = flatten
259 .iter()
260 .find(|f| f.name == HIT_ID || f.name == HIT_PATH)?;
261 Some(SurfaceError::filter_invalid(
262 format!(
263 "flatten `as {0}` collides with the hit envelope — a flattened row is still the row, whose `{0}` the envelope reads; bind another name",
264 f.name
265 ),
266 "OQX",
267 ))
268}
269
270fn root_literal(e: Expr) -> Expr {
271 match e {
272 Expr::Lit {
273 value: Value::Str(s),
274 span,
275 } => Expr::Lit {
276 value: Value::Str(reference_path(&s)),
277 span,
278 },
279 other => other,
280 }
281}
282
283#[must_use]
285pub fn root_path_literals(e: Expr) -> Expr {
286 match e {
287 Expr::Binary {
288 op: op @ (BinaryOp::Eq | BinaryOp::Ne),
289 left,
290 right,
291 span,
292 } => {
293 if is_path_read(&left) {
294 Expr::Binary {
295 op,
296 left,
297 right: Box::new(root_literal(*right)),
298 span,
299 }
300 } else if is_path_read(&right) {
301 Expr::Binary {
302 op,
303 left: Box::new(root_literal(*left)),
304 right,
305 span,
306 }
307 } else {
308 Expr::Binary {
309 op,
310 left,
311 right,
312 span,
313 }
314 }
315 }
316 Expr::Call {
317 recv: Some(recv),
318 name,
319 mut args,
320 span,
321 } if name == "startsWith" && args.len() == 1 && is_path_read(&recv) => {
322 let arg = root_literal(args.remove(0));
323 Expr::Call {
324 recv: Some(recv),
325 name,
326 args: vec![arg],
327 span,
328 }
329 }
330 other => other,
331 }
332}
333
334#[must_use]
346pub fn fold_dates(e: Expr, now_ms: i64) -> Expr {
347 let Expr::Call {
348 recv, name, args, ..
349 } = &e
350 else {
351 return e;
352 };
353 let mut vals = Vec::with_capacity(args.len());
354 for a in args {
355 let Expr::Lit { value, .. } = a else { return e };
356 vals.push(value.clone());
357 }
358 let folded = match recv {
359 None => {
360 if dates::date_function_arity(name) != Some(vals.len()) {
361 return e;
362 }
363 dates::date_function(name, &vals, now_ms)
364 }
365 Some(r) => {
366 let Expr::Lit { value, .. } = &**r else {
367 return e;
368 };
369 if dates::date_method_arity(name) != Some(vals.len()) {
370 return e;
371 }
372 dates::date_method(name, value, &vals)
373 }
374 };
375 match folded {
376 Some(Ok(v)) => build::lit(v),
377 _ => e,
378 }
379}
380
381#[must_use]
386pub fn rewrite_query(q: &Query, now_ms: i64) -> Query {
387 transform(q, &mut |e, _| match e {
388 Expr::Call {
389 recv: None,
390 name,
391 args,
392 span,
393 } if ROW_FNS.contains(&name.as_str()) => Expr::Call {
394 recv: Some(Box::new(build::ident("$self"))),
395 name,
396 args,
397 span,
398 },
399 other => fold_dates(root_path_literals(other), now_ms),
400 })
401}
402
403pub 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`";
407
408#[must_use]
412pub fn bare_target_message(t: Target, depth: Option<usize>) -> String {
413 let name = t.as_str();
414 let noun = t.noun();
415 let carets = "^".repeat(depth.unwrap_or(1));
416 let how = if depth.is_none() {
417 format!("; one caret per enclosing block, or the absolute `0^{name}`")
418 } else {
419 String::new()
420 };
421 format!(
422 "`{name}` inside a block reads a property of the current row, which has none — did you mean `{carets}{name}` (the repository's {noun}{how})?"
423 )
424}
425
426#[must_use]
432pub fn unknown_target_message(name: &str) -> String {
433 let was = if name == "edges" {
434 " (`edges` was renamed `links` in surface 3.0)"
435 } else {
436 ""
437 };
438 let roots: Vec<String> = crate::context::ALL_TARGETS
439 .iter()
440 .map(|t| format!("`{}`", t.as_str()))
441 .collect();
442 format!(
443 "`{name}` is not a target — the repository's collections are {}{was}",
444 roots.join(", ")
445 )
446}
447
448#[must_use]
452pub fn past_root_message(t: Target, levels: usize) -> String {
453 let name = t.as_str();
454 let noun = t.noun();
455 let carets = "^".repeat(levels);
456 format!(
457 "`{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})`)"
458 )
459}
460
461pub fn check_root_spellings(q: &Query) -> Result<()> {
475 if let Expr::Ident { name, .. } = &q.source {
479 if Target::parse(name).is_none() {
480 return Err(SurfaceError::filter_invalid(
481 unknown_target_message(name),
482 "OQX",
483 ));
484 }
485 }
486 struct Check(Option<SurfaceError>);
487 impl Visitor<'_> for Check {
488 fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
489 if self.0.is_some() {
490 return false;
491 }
492 match node {
493 Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. })
494 if name == "$repo" =>
495 {
496 self.0 = Some(SurfaceError::filter_invalid(REPO_REMOVED_MESSAGE, "OQX"));
497 }
498 Node::Expr(Expr::Ident { name, .. })
499 if ctx.depth >= 1 && matches!(name.as_str(), "docs" | "links") =>
500 {
501 let t = Target::parse(name).expect("a target name");
502 self.0 = Some(SurfaceError::filter_invalid(
503 bare_target_message(t, Some(ctx.depth)),
504 "OQX",
505 ));
506 }
507 Node::Expr(Expr::Outer { name, levels, .. }) if *levels > ctx.depth => {
508 if let Some(t) = Target::parse(name) {
509 self.0 = Some(SurfaceError::filter_invalid(
510 past_root_message(t, *levels),
511 "OQX",
512 ));
513 }
514 }
515 _ => {}
516 }
517 self.0.is_none()
518 }
519 }
520 let mut check = Check(None);
521 visit(Node::Query(q), &mut check);
522 check.0.map_or(Ok(()), Err)
523}
524
525struct Phrases(Vec<String>);
529
530impl Visitor<'_> for Phrases {
531 fn enter(&mut self, node: Node<'_>, _ctx: &VisitContext<'_>) -> bool {
532 if let Node::Expr(Expr::Call {
533 recv: None,
534 name,
535 args,
536 ..
537 }) = node
538 && name == "semantic"
539 && let Some(Expr::Lit {
540 value: Value::Str(s),
541 ..
542 }) = args.first()
543 && !self.0.contains(s)
544 {
545 self.0.push(s.clone());
546 }
547 true
548 }
549}
550
551#[must_use]
554pub fn collect_semantic_phrases(source: &str) -> Vec<String> {
555 let Ok(q) = oqx::parse_string(source) else {
556 return Vec::new();
557 };
558 semantic_phrases_of(&q)
559}
560
561#[must_use]
563pub fn semantic_phrases_of(q: &Query) -> Vec<String> {
564 let mut phrases = Phrases(Vec::new());
565 visit(Node::Query(q), &mut phrases);
566 phrases.0
567}
568
569pub fn embed_phrases(
576 provider: Option<&dyn EmbeddingProvider>,
577 phrases: Vec<String>,
578) -> Result<HashMap<String, SemanticVec>> {
579 let mut semantic: HashMap<String, SemanticVec> = HashMap::new();
580 if let Some(provider) = provider.filter(|_| !phrases.is_empty()) {
581 for phrase in phrases {
582 let vec = provider
583 .embed_query(&phrase)
584 .map_err(|e| SurfaceError::new(e.code(), e.to_string()))?;
585 semantic.insert(
586 phrase,
587 SemanticVec {
588 model: provider.model().to_owned(),
589 vec: f32_to_blob(&vec),
590 },
591 );
592 }
593 }
594 Ok(semantic)
595}
596
597pub const RESERVED_ALIAS_PREFIXES: [&str; 2] = ["__oqx_", "__omg_"];
608
609pub fn check_reserved_aliases(q: &Query) -> Result<()> {
611 for item in &q.select {
612 if let Some(prefix) = RESERVED_ALIAS_PREFIXES
613 .iter()
614 .find(|p| item.name().starts_with(*p))
615 {
616 return Err(SurfaceError::filter_invalid(
617 format!(
618 "projection `{}` uses the reserved prefix `{prefix}` — choose another alias",
619 item.name()
620 ),
621 "OQX",
622 ));
623 }
624 }
625 Ok(())
626}
627
628fn mentions_outside_source(q: &Query, name: &str) -> bool {
634 struct Mentions<'n> {
635 name: &'n str,
636 found: bool,
637 }
638 impl Visitor<'_> for Mentions<'_> {
639 fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
640 if self.found {
641 return false;
642 }
643 if ctx.clause == Some(Clause::Source) && ctx.path.len() == 1 {
645 return false;
646 }
647 if let Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. }) = node
648 && name == self.name
649 {
650 self.found = true;
651 }
652 !self.found
653 }
654 }
655 let mut m = Mentions { name, found: false };
656 visit(Node::Query(q), &mut m);
657 m.found
658}
659
660fn may_reach_non_rows(q: &Query) -> bool {
668 q.follow.is_some() || !q.from.is_empty() || root_target(&q.source).is_none()
669}
670
671fn describe_value(v: &Value) -> String {
673 match v {
674 Value::Undefined | Value::Null => "an absent value".to_owned(),
675 Value::Str(s) => format!(
676 "a string ({})",
677 serde_json::to_string(s).unwrap_or_default()
678 ),
679 Value::Number(_) => format!("a number ({v})"),
680 Value::Bool(b) => format!("a boolean ({b})"),
681 Value::Array(_) => "an array".to_owned(),
682 Value::Range(_) => "a range".to_owned(),
683 Value::Object(_) => "an object".to_owned(),
684 }
685}
686
687fn not_a_store_row(v: &Value) -> SurfaceError {
688 SurfaceError::filter_invalid(
689 format!(
690 "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>)",
691 describe_value(v)
692 ),
693 "OQX",
694 )
695}
696
697#[derive(Debug)]
707struct Shaped {
708 id: String,
709 path: String,
710 proj: oqx::Object,
711}
712
713fn path_string(path: &Value) -> String {
714 if path.is_absent() {
715 String::new()
716 } else {
717 path.to_string()
718 }
719}
720
721fn shape_row(row: Value) -> Result<Shaped> {
722 let mut shaped = Shaped {
723 id: Value::Undefined.to_string(),
724 path: String::new(),
725 proj: oqx::Object::new(),
726 };
727 let Value::Object(mut o) = row else {
728 return Ok(shaped);
729 };
730 if let Some(me) = o.remove(SELF_KEY)
731 && target_of(&me).is_none()
732 {
733 return Err(not_a_store_row(&me));
734 }
735 let Value::Object(o) = render_row_values(Value::Object(o)) else {
736 return Ok(shaped);
737 };
738 for (k, v) in o {
739 match k.as_str() {
740 ID_KEY => shaped.id = v.to_string(),
741 PATH_KEY => shaped.path = path_string(&v),
742 _ => {
743 shaped.proj.insert(k, v);
744 }
745 }
746 }
747 Ok(shaped)
748}
749
750fn raw_identity(row: &Value) -> (String, String) {
755 let get = |k: &str| {
756 row.as_object()
757 .and_then(|o| o.get(k))
758 .cloned()
759 .unwrap_or(Value::Undefined)
760 };
761 (path_string(&get(PATH_KEY)), get(ID_KEY).to_string())
762}
763
764fn to_hit(s: Shaped) -> Value {
769 let mut hit = oqx::Object::with_capacity(s.proj.len() + 2);
770 hit.insert(HIT_ID, Value::Str(s.id));
771 hit.insert(HIT_PATH, Value::Str(s.path));
772 for (k, v) in s.proj {
773 hit.insert(k, v);
774 }
775 Value::Object(hit)
776}
777
778fn to_bare_hit(s: Shaped) -> Value {
783 Value::Object(s.proj)
784}
785
786fn render_hit(s: Shaped, bare: bool) -> Json {
788 if bare {
789 to_bare_hit(s).to_canonical_json()
790 } else {
791 to_hit(s).to_canonical_json()
792 }
793}
794
795fn dedup_by_projection(rows: Vec<Shaped>, bare: bool) -> Vec<Shaped> {
804 let mut seen: Vec<String> = Vec::new();
805 let mut out = Vec::new();
806 for s in rows {
807 let mut pairs: Vec<(String, Value)> = s
808 .proj
809 .iter()
810 .filter(|(k, _)| bare || (*k != HIT_ID && *k != HIT_PATH))
811 .map(|(k, v)| (k.to_owned(), v.clone()))
812 .collect();
813 pairs.sort_by(|a, b| a.0.cmp(&b.0));
814 let key = Value::Array(
815 pairs
816 .into_iter()
817 .map(|(k, v)| Value::Array(vec![Value::Str(k), v]))
818 .collect(),
819 )
820 .to_canonical_json()
821 .to_string();
822 if seen.contains(&key) {
823 continue;
824 }
825 seen.push(key);
826 out.push(s);
827 }
828 out
829}
830
831fn const_bound(e: Option<&Expr>, word: &str) -> Result<Option<usize>> {
834 match e {
835 None => Ok(None),
836 Some(Expr::Lit {
837 value: Value::Number(n),
838 ..
839 }) if n.fract() == 0.0 && *n >= 0.0 && n.is_finite() => Ok(Some(*n as usize)),
840 Some(_) => Err(SurfaceError::filter_invalid(
841 format!("top-level {word} must be a non-negative integer literal"),
842 "OQX",
843 )),
844 }
845}
846
847fn page_rows<T>(
853 mut rows: Vec<T>,
854 ident: impl Fn(&T) -> (String, String),
855 offset: usize,
856 limit: Option<usize>,
857 cursor: Option<&str>,
858 cap: usize,
859 custom: bool,
860) -> Result<(Vec<T>, bool, Option<String>)> {
861 if offset > 0 || limit.is_some() {
862 let end = limit.map_or(rows.len(), |l| (offset + l).min(rows.len()));
863 if offset >= rows.len() {
864 rows.clear();
865 } else {
866 rows.truncate(end);
867 rows.drain(..offset);
868 }
869 }
870 if let Some(cursor) = cursor {
871 let parts = decode_path_cursor(cursor, "query", 2)?;
873 let (path, id) = (&parts[0], &parts[1]);
874 rows.retain(|r| {
875 let (rp, ri) = ident(r);
876 rp > *path || (rp == *path && ri > *id)
877 });
878 }
879 let truncated = rows.len() > cap;
880 rows.truncate(cap);
881 let next = if truncated && !custom {
882 rows.last().map(|last| {
883 let (p, i) = ident(last);
884 encode_cursor(&[&p, &i])
885 })
886 } else {
887 None
888 };
889 Ok((rows, truncated, next))
890}
891
892fn value_of(s: &Shaped) -> Json {
893 s.proj
894 .get(VALUE_KEY)
895 .cloned()
896 .unwrap_or(Value::Undefined)
897 .to_canonical_json()
898}
899
900pub fn check_bare(q: &Query) -> Result<()> {
907 if q.values {
908 return Err(SurfaceError::filter_invalid(
909 BARE_WITH_VALUES_MESSAGE,
910 "OQX",
911 ));
912 }
913 if q.select.is_empty() {
914 return Err(SurfaceError::filter_invalid(
915 BARE_NEEDS_SELECT_MESSAGE,
916 "OQX",
917 ));
918 }
919 Ok(())
920}
921
922pub const BARE_NEEDS_SELECT_MESSAGE: &str =
924 "`bare` needs a `select` — without a projection a hit is only its envelope";
925pub const BARE_WITH_VALUES_MESSAGE: &str =
926 "`bare` is redundant with `values` — `values` is already the bare-values form; drop one";
927
928pub fn query(
932 store: &Store,
933 repo_id: &str,
934 source: &str,
935 opts: QueryOptions<'_>,
936) -> Result<OqxResult> {
937 let semantic = embed_phrases(opts.provider, collect_semantic_phrases(source))?;
938 let runner = Runner {
939 store,
940 repo_id,
941 semantic,
942 planned: !opts.in_memory,
943 now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
944 };
945 let raw = oqx::parse_string(source)?;
946 check_reserved_aliases(&raw)?;
947 run_parsed(
949 &runner,
950 &raw,
951 opts,
952 opts.unknown_names.unwrap_or(UnknownNames::Hint),
953 )
954}
955
956pub fn query_ast(
965 store: &Store,
966 repo_id: &str,
967 raw: &Query,
968 opts: QueryOptions<'_>,
969 semantic: HashMap<String, SemanticVec>,
970) -> Result<OqxResult> {
971 let runner = Runner {
972 store,
973 repo_id,
974 semantic,
975 planned: !opts.in_memory,
976 now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
977 };
978 run_parsed(
979 &runner,
980 raw,
981 opts,
982 opts.unknown_names.unwrap_or(UnknownNames::Refuse),
983 )
984}
985
986struct Runner<'a> {
992 store: &'a Store,
993 repo_id: &'a str,
994 semantic: HashMap<String, SemanticVec>,
995 planned: bool,
996 now_ms: i64,
998}
999
1000impl Runner<'_> {
1001 fn run(&self, q: &Query) -> Result<oqx::OqxResult> {
1008 let conn = self.store.conn();
1009 let ctx =
1010 StoreContext::new(conn, self.repo_id, self.semantic.clone()).with_now_ms(self.now_ms);
1011 let plan = if self.planned {
1012 SqlitePlanner::new(conn, self.repo_id)
1013 .try_plan(q, &[])
1014 .map_err(|e| SurfaceError::other(format!("sqlite: {e}")))?
1015 } else {
1016 None
1017 };
1018 let (ctx, residual) = match plan {
1019 Some(plan) => {
1024 let once = !mentions_outside_source(&plan.residual, oqx::ROWS_ROOT);
1025 let ctx = if once {
1026 ctx.with_rows_root_once(plan.rows)
1027 } else {
1028 ctx.with_rows_root(plan.rows)
1029 };
1030 (ctx, Some(plan.residual))
1031 }
1032 None => (ctx, None),
1033 };
1034 let engine = InMemoryEngine::new(ctx);
1035 let out = engine.run(residual.as_ref().unwrap_or(q), &[]);
1036 if let Some(failed) = engine.context().take_root_failure() {
1039 return Err(failed.into());
1040 }
1041 Ok(out?)
1044 }
1045}
1046
1047fn run_parsed(
1048 engine: &Runner<'_>,
1049 raw: &Query,
1050 opts: QueryOptions<'_>,
1051 mode: UnknownNames,
1052) -> Result<OqxResult> {
1053 check_root_spellings(raw)?;
1054 let own = Query {
1062 select: raw
1063 .select
1064 .iter()
1065 .filter(|it| {
1066 !RESERVED_ALIAS_PREFIXES
1067 .iter()
1068 .any(|p| it.name().starts_with(p))
1069 })
1070 .cloned()
1071 .collect(),
1072 ..raw.clone()
1073 };
1074 let local = VocabularyCache::default();
1075 let cache = opts.vocabulary.unwrap_or(&local);
1076 let hints = check_names(engine.store, engine.repo_id, &own, mode, cache)?;
1077 let mut result = run_resolved(engine, raw, opts)?;
1078 result.hints = hints;
1080 Ok(result)
1081}
1082
1083fn run_resolved(engine: &Runner<'_>, raw: &Query, opts: QueryOptions<'_>) -> Result<OqxResult> {
1084 let parsed = rewrite_query(&resolve_aliases(raw)?, engine.now_ms);
1087 let consumer = parsed.consumer;
1088
1089 match consumer {
1090 Consumer::Exists => {
1091 let res = engine.run(&parsed)?;
1092 let mut r = OqxResult::scalar(consumer);
1093 r.exists = Some(matches!(res, oqx::OqxResult::Exists(true)));
1094 return Ok(r);
1095 }
1096 Consumer::Count => {
1097 let res = engine.run(&parsed)?;
1098 let mut r = OqxResult::scalar(consumer);
1099 r.count = Some(match res {
1100 oqx::OqxResult::Count(n) => n,
1101 _ => 0.0,
1102 });
1103 return Ok(r);
1104 }
1105 Consumer::None => {
1106 let res = engine.run(&parsed)?;
1107 let mut r = OqxResult::scalar(consumer);
1108 r.none = Some(match res {
1109 oqx::OqxResult::None(b) => b,
1110 _ => true,
1111 });
1112 return Ok(r);
1113 }
1114 Consumer::Collect | Consumer::First | Consumer::Single => {}
1115 }
1116
1117 if parsed.group_by.is_some() {
1129 return run_grouped(engine, &parsed, consumer, opts);
1130 }
1131
1132 let flattened = !parsed.flatten.is_empty();
1143 if let Some(e) = flatten_collision(&parsed.flatten) {
1144 return Err(e);
1145 }
1146
1147 let top_distinct = parsed.distinct;
1153 let top_values = parsed.values;
1154 let user_select: Vec<SelectItem> = if top_values {
1155 parsed
1156 .select
1157 .first()
1158 .map(|it| match it {
1159 SelectItem::Field {
1160 expr, lift, span, ..
1161 } => SelectItem::Field {
1162 name: VALUE_KEY.to_owned(),
1163 expr: expr.clone(),
1164 lift: *lift,
1165 span: *span,
1166 },
1167 SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1168 name: VALUE_KEY.to_owned(),
1169 op: op.clone(),
1170 span: *span,
1171 },
1172 })
1173 .into_iter()
1174 .collect()
1175 } else {
1176 parsed.select.clone()
1177 };
1178 let bare = opts.bare;
1182 if bare {
1183 check_bare(&parsed)?;
1184 } else if !top_values && let Some(e) = envelope_collision(&user_select) {
1185 return Err(e);
1186 }
1187 let id_item = build::field(ID_KEY, build::ident("id"));
1188 let path_item = build::field(PATH_KEY, build::ident("path"));
1189 let mut select = vec![id_item, path_item];
1190 if !top_values && may_reach_non_rows(&parsed) {
1191 select.push(build::field(SELF_KEY, build::ident("$it")));
1192 }
1193 select.extend(user_select);
1194 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1198 let q = Query {
1199 distinct: false,
1200 values: false,
1201 select,
1202 limit: if consumer == Consumer::Collect {
1203 None
1204 } else {
1205 parsed.limit.clone()
1206 },
1207 offset: if consumer == Consumer::Collect {
1208 None
1209 } else {
1210 parsed.offset.clone()
1211 },
1212 ..parsed.clone()
1213 };
1214 let res = engine.run(&q)?;
1215
1216 if matches!(consumer, Consumer::First | Consumer::Single) {
1217 let row = match res {
1218 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1219 _ => None,
1220 };
1221 let mut out = OqxResult::scalar(consumer);
1222 match row {
1223 None => {
1224 if top_values {
1225 out.values = Some(Vec::new());
1226 }
1227 }
1228 Some(r) => {
1229 let shaped = shape_row(r)?;
1230 if top_values {
1231 out.values = Some(vec![value_of(&shaped)]);
1232 } else {
1233 out.hits = vec![render_hit(shaped, bare)];
1234 }
1235 }
1236 }
1237 return Ok(out);
1238 }
1239
1240 let rows: Vec<Value> = match res {
1242 oqx::OqxResult::Collect(rows) => rows,
1243 _ => Vec::new(),
1244 };
1245 let custom = parsed.order_by.as_ref().is_some_and(|o| !o.is_empty()) || flattened;
1246 let cursor = opts.cursor.filter(|c| !c.is_empty() && !custom);
1247 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1248 let limit = const_bound(top_limit.as_ref(), "limit")?;
1249 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1250 let (page, truncated, next) = if top_distinct {
1256 let shaped = rows
1257 .into_iter()
1258 .map(shape_row)
1259 .collect::<Result<Vec<_>>>()?;
1260 let shaped = dedup_by_projection(shaped, bare);
1261 page_rows(
1262 shaped,
1263 |s| (s.path.clone(), s.id.clone()),
1264 offset,
1265 limit,
1266 cursor,
1267 cap,
1268 custom,
1269 )?
1270 } else {
1271 let (page, truncated, next) =
1272 page_rows(rows, raw_identity, offset, limit, cursor, cap, custom)?;
1273 let page = page
1274 .into_iter()
1275 .map(shape_row)
1276 .collect::<Result<Vec<_>>>()?;
1277 (page, truncated, next)
1278 };
1279 let mut out = OqxResult::scalar(Consumer::Collect);
1280 out.truncated = truncated;
1281 out.cursor = next;
1282 if top_values {
1283 out.values = Some(page.iter().map(value_of).collect());
1284 } else {
1285 out.hits = page.into_iter().map(|s| render_hit(s, bare)).collect();
1286 }
1287 Ok(out)
1288}
1289
1290fn run_grouped(
1292 engine: &Runner<'_>,
1293 parsed: &Query,
1294 consumer: Consumer,
1295 opts: QueryOptions<'_>,
1296) -> Result<OqxResult> {
1297 let top_values = parsed.values;
1298 let select: Vec<SelectItem> = if top_values {
1301 parsed
1302 .select
1303 .first()
1304 .map(|it| match it {
1305 SelectItem::Field {
1306 expr, lift, span, ..
1307 } => SelectItem::Field {
1308 name: VALUE_KEY.to_owned(),
1309 expr: expr.clone(),
1310 lift: *lift,
1311 span: *span,
1312 },
1313 SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1314 name: VALUE_KEY.to_owned(),
1315 op: op.clone(),
1316 span: *span,
1317 },
1318 })
1319 .into_iter()
1320 .collect()
1321 } else {
1322 parsed.select.clone()
1323 };
1324 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1325 let q = Query {
1326 values: false,
1327 select,
1328 limit: if consumer == Consumer::Collect {
1329 None
1330 } else {
1331 parsed.limit.clone()
1332 },
1333 offset: if consumer == Consumer::Collect {
1334 None
1335 } else {
1336 parsed.offset.clone()
1337 },
1338 ..parsed.clone()
1339 };
1340 let res = engine.run(&q)?;
1341 let project = |row: Value| -> oqx::Object {
1342 match render_row_values(row) {
1343 Value::Object(o) => o,
1344 _ => oqx::Object::new(),
1345 }
1346 };
1347 let value_of = |g: &oqx::Object| -> Json {
1348 g.get(VALUE_KEY)
1349 .cloned()
1350 .unwrap_or(Value::Undefined)
1351 .to_canonical_json()
1352 };
1353 if matches!(consumer, Consumer::First | Consumer::Single) {
1354 let row = match res {
1355 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1356 _ => None,
1357 };
1358 let mut out = OqxResult::scalar(consumer);
1359 match row {
1360 None => {
1361 if top_values {
1362 out.values = Some(Vec::new());
1363 }
1364 }
1365 Some(r) => {
1366 let g = project(r);
1367 if top_values {
1368 out.values = Some(vec![value_of(&g)]);
1369 } else {
1370 out.hits = vec![Value::Object(g).to_canonical_json()];
1371 }
1372 }
1373 }
1374 return Ok(out);
1375 }
1376 let mut groups: Vec<oqx::Object> = match res {
1377 oqx::OqxResult::Collect(rows) => rows.into_iter().map(project).collect(),
1378 _ => Vec::new(),
1379 };
1380 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1381 let limit = const_bound(top_limit.as_ref(), "limit")?;
1382 if offset > 0 || limit.is_some() {
1383 let end = limit.map_or(groups.len(), |l| (offset + l).min(groups.len()));
1384 if offset >= groups.len() {
1385 groups.clear();
1386 } else {
1387 groups.truncate(end);
1388 groups.drain(..offset);
1389 }
1390 }
1391 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1392 let truncated = groups.len() > cap;
1393 groups.truncate(cap);
1394 let mut out = OqxResult::scalar(Consumer::Collect);
1395 out.truncated = truncated;
1396 if top_values {
1397 out.values = Some(groups.iter().map(value_of).collect());
1398 } else {
1399 out.hits = groups
1400 .into_iter()
1401 .map(|g| Value::Object(g).to_canonical_json())
1402 .collect();
1403 }
1404 Ok(out)
1405}
1406
1407#[cfg(test)]
1408mod tests {
1409 use super::*;
1410 use oqx::ast::Where;
1411
1412 #[test]
1413 fn row_functions_become_self_methods() {
1414 let q = oqx::parse_string(
1415 "from blocks where text(\"x\") && under_heading(\"h\") && size(attrs) > 0 && doc.path.startsWith(\"a\")",
1416 )
1417 .unwrap();
1418 let r = rewrite_query(&q, 0);
1419 let Some(Where::And { parts, .. }) = &r.r#where else {
1420 panic!("and")
1421 };
1422 let Where::Scalar { expr, .. } = &parts[0] else {
1423 panic!("scalar")
1424 };
1425 assert!(
1426 matches!(expr, Expr::Call { recv: Some(r), name, .. } if name == "text" && **r == build::ident("$self"))
1427 );
1428 let Where::Scalar { expr, .. } = &parts[2] else {
1429 panic!("scalar")
1430 };
1431 assert!(
1432 matches!(expr, Expr::Binary { left, .. } if matches!(&**left, Expr::Call { recv: None, name, .. } if name == "size"))
1433 );
1434 }
1435
1436 const NOW: i64 = 1_790_467_200_000;
1440
1441 fn where_expr(src: &str) -> Expr {
1442 let q = oqx::parse_string(&format!("from docs where {src}")).unwrap();
1443 let Some(Where::Scalar { expr, .. }) = q.r#where else {
1444 panic!("not a scalar where")
1445 };
1446 expr
1447 }
1448
1449 fn fold(src: &str) -> Expr {
1451 fn go(e: Expr) -> Expr {
1452 match e {
1453 Expr::Call {
1454 recv,
1455 name,
1456 args,
1457 span,
1458 } => fold_dates(
1459 Expr::Call {
1460 recv: recv.map(|r| Box::new(go(*r))),
1461 name,
1462 args: args.into_iter().map(go).collect(),
1463 span,
1464 },
1465 NOW,
1466 ),
1467 other => other,
1468 }
1469 }
1470 go(where_expr(src))
1471 }
1472
1473 fn lit(e: &Expr) -> Option<&Value> {
1474 match e {
1475 Expr::Lit { value, .. } => Some(value),
1476 _ => None,
1477 }
1478 }
1479
1480 #[test]
1481 fn folds_date_dur_over_literals_and_date_methods_over_literal_receivers_and_arguments() {
1482 assert_eq!(
1483 lit(&fold("date(\"2026-01-05\")")),
1484 Some(&Value::from("2026-01-05"))
1485 );
1486 assert_eq!(
1487 lit(&fold("date(\"today\")")),
1488 Some(&Value::from("2026-09-27"))
1489 );
1490 assert_eq!(
1491 lit(&fold("dur(\"30 days\")")),
1492 Some(&Value::from(30.0 * 86400.0))
1493 );
1494 assert_eq!(
1495 lit(&fold("date(\"today\").minus(dur(\"30 days\"))")),
1496 Some(&Value::from("2026-08-28"))
1497 );
1498 assert_eq!(lit(&fold("date(\"x\")")), Some(&Value::Null));
1499 assert_eq!(lit(&fold("date(5)")), Some(&Value::Null));
1500 assert_eq!(lit(&fold("dur(60).dur_text()")), Some(&Value::from("1m")));
1501 }
1502
1503 #[test]
1504 fn leaves_a_non_literal_argument_an_unknown_name_and_a_wrong_arity_in_place() {
1505 for src in [
1506 "date(when)",
1507 "date(\"today\").until(when)",
1508 "size(\"x\")",
1509 "date()",
1510 "date(\"2026-01-01\").plus()",
1511 "\"x\".lower()",
1512 ] {
1513 assert!(matches!(fold(src), Expr::Call { .. }), "{src}");
1514 }
1515 }
1516
1517 #[test]
1518 fn the_runner_folds_at_its_clock_so_a_named_point_pushes_down() {
1519 let q = oqx::parse_string("from docs where updated_at < date(\"today\") && date(when) < date(\"today\").minus(dur(\"30 days\"))").unwrap();
1520 let printed = oqx::print_query(&rewrite_query(&q, NOW)).unwrap();
1521 assert!(printed.contains("updated_at < \"2026-09-27\""), "{printed}");
1522 assert!(printed.contains("date(when) < \"2026-08-28\""), "{printed}");
1523 }
1524
1525 #[test]
1526 fn path_literals_are_rooted_only_against_path_reads() {
1527 let q = oqx::parse_string(
1528 "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\"",
1529 )
1530 .unwrap();
1531 let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1532 assert!(printed.contains("path.startsWith(\"/lab/\")"), "{printed}");
1533 assert!(printed.contains("doc.path == \"/a.md\""), "{printed}");
1534 assert!(printed.contains("type == \"a.md\""), "{printed}");
1535 assert!(printed.contains("^path == \"/b.md\""), "{printed}");
1536 assert!(printed.contains("\"/x.md\" != dst_path"), "{printed}");
1537 assert!(printed.contains("path == ^after"), "{printed}");
1539 let q = oqx::parse_string(
1541 "from docs where path.contains(\"a/\") && path.endsWith(\".md\") && path == \"\"",
1542 )
1543 .unwrap();
1544 let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1545 assert!(printed.contains("path.contains(\"a/\")"), "{printed}");
1546 assert!(printed.contains("path.endsWith(\".md\")"), "{printed}");
1547 assert!(printed.contains("path == \"/\""), "{printed}");
1548 }
1549
1550 #[test]
1551 fn repo_and_bare_targets_are_refused_statically() {
1552 let parse = |s: &str| oqx::parse_string(s).unwrap();
1553 for q in [
1554 "from docs where $repo",
1555 "select r: size($repo.docs) from docs",
1556 "select r: $repo.id from docs",
1557 "select r: nodes first { select x: ^$repo.id values } from docs",
1558 "select r: nodes first { select x: 0^$repo values } from docs",
1559 "$repo.docs count { }",
1560 ] {
1561 let e = check_root_spellings(&parse(q)).unwrap_err();
1562 assert_eq!(e.code, "filter_invalid", "{q}");
1563 assert_eq!(e.message, REPO_REMOVED_MESSAGE, "{q}");
1564 }
1565 let e = check_root_spellings(&parse("select x: docs collect { } from docs")).unwrap_err();
1567 assert_eq!(e.message, bare_target_message(Target::Docs, Some(1)));
1568 assert!(
1569 e.message
1570 .contains("did you mean `^docs` (the repository's documents)?")
1571 );
1572 let e = check_root_spellings(&parse(
1573 "select x: nodes collect { select y: size(links) } from docs",
1574 ))
1575 .unwrap_err();
1576 assert!(
1577 e.message
1578 .contains("did you mean `^^links` (the repository's links)?")
1579 );
1580 let e = check_root_spellings(&parse("docs collect { from docs }")).unwrap_err();
1581 assert!(e.message.contains("`^docs`"));
1582 for q in [
1584 "docs count { }",
1585 "from docs where nodes exists { }",
1586 "entries(docs) first { }",
1587 "select n: size(^docs), id: ^id, k: size(0^links) from docs",
1588 "from blocks where blocks exists { }", ] {
1590 check_root_spellings(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1591 }
1592 let e = check_root_spellings(&parse("^docs count { }")).unwrap_err();
1594 assert_eq!(e.message, past_root_message(Target::Docs, 1));
1595 assert_eq!(
1596 e.message,
1597 "`^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)`)"
1598 );
1599 let e = check_root_spellings(&parse("select path from docs where ^^links exists { }"))
1600 .unwrap_err();
1601 assert!(
1602 e.message.contains("`^^links` reaches past the root"),
1603 "{}",
1604 e.message
1605 );
1606 let m = bare_target_message(Target::Blocks, None);
1608 assert!(m.contains("did you mean `^blocks` (the repository's blocks; one caret per enclosing block, or the absolute `0^blocks`)?"), "{m}");
1609 }
1610
1611 #[test]
1612 fn reserved_projection_names_are_refused() {
1613 let parse = |s: &str| oqx::parse_string(s).unwrap();
1614 let e = check_reserved_aliases(&parse("select __oqx_id: 1 from docs")).unwrap_err();
1615 assert_eq!(e.code, "filter_invalid");
1616 assert_eq!(
1617 e.message,
1618 "projection `__oqx_id` uses the reserved prefix `__oqx_` — choose another alias"
1619 );
1620 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1621 let e = check_reserved_aliases(&parse("select __omg_sel_0_x: 1 from docs")).unwrap_err();
1622 assert!(
1623 e.message.contains("reserved prefix `__omg_`"),
1624 "{}",
1625 e.message
1626 );
1627 assert!(
1629 check_reserved_aliases(&parse(
1630 "select __oqx: 1, x: nodes collect { select __oqx_y: 1 } from docs"
1631 ))
1632 .is_ok()
1633 );
1634 }
1635
1636 #[test]
1637 fn semantic_phrases_are_collected_distinct() {
1638 let phrases = collect_semantic_phrases(
1639 "select s: semantic(\"alpha\") from docs where semantic(\"alpha\") > 0.5 || nodes exists { where semantic(\"beta\") > 0 } order by semantic(\"gamma\") desc",
1640 );
1641 assert_eq!(phrases, ["alpha", "beta", "gamma"]);
1642 assert!(collect_semantic_phrases("not a query {{").is_empty());
1643 assert!(collect_semantic_phrases("from docs").is_empty());
1644 }
1645
1646 #[test]
1647 fn hits_peel_the_injected_columns() {
1648 let mut o = oqx::Object::new();
1649 o.insert(ID_KEY, Value::Str("d_1".into()));
1650 o.insert(PATH_KEY, Value::Null);
1651 o.insert("layer", Value::Str("canon".into()));
1652 let shaped = shape_row(Value::Object(o)).unwrap();
1653 assert_eq!((shaped.id.as_str(), shaped.path.as_str()), ("d_1", ""));
1654 let hit = to_hit(shaped);
1655 let ho = hit.as_object().unwrap();
1656 assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path", "layer"]);
1657 assert_eq!(ho.get("path"), Some(&Value::Str(String::new())));
1658 let mut o = oqx::Object::new();
1661 o.insert(ID_KEY, Value::Str("d_1".into()));
1662 o.insert(PATH_KEY, Value::Str("a.md".into()));
1663 o.insert("id", Value::Number(7.0));
1664 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1665 let ho = hit.as_object().unwrap();
1666 assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path"]);
1667 assert_eq!(ho.get("id"), Some(&Value::Number(7.0)));
1668 let mut o = oqx::Object::new();
1670 o.insert(ID_KEY, Value::Str("d_1".into()));
1671 o.insert(PATH_KEY, Value::Undefined);
1672 let row = Value::Object(o);
1673 assert_eq!(raw_identity(&row), (String::new(), "d_1".to_owned()));
1674 let shaped = shape_row(row).unwrap();
1675 assert_eq!((shaped.path, shaped.id), (String::new(), "d_1".to_owned()));
1676 }
1677
1678 #[test]
1679 fn bare_hits_are_the_projection_in_its_own_order() {
1680 let mut o = oqx::Object::new();
1683 o.insert(ID_KEY, Value::Str("d_1".into()));
1684 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1685 o.insert("title", Value::Str("T".into()));
1686 o.insert("id", Value::Str("d_1".into()));
1687 let shaped = shape_row(Value::Object(o)).unwrap();
1688 let bare = to_bare_hit(shaped);
1689 let bo = bare.as_object().unwrap();
1690 assert_eq!(bo.keys().collect::<Vec<_>>(), ["title", "id"]);
1691 let mut o = oqx::Object::new();
1694 o.insert(ID_KEY, Value::Str("d_1".into()));
1695 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1696 o.insert("path", Value::Str("canon".into()));
1697 let shaped = shape_row(Value::Object(o)).unwrap();
1698 assert_eq!(shaped.path, "/a.md");
1699 assert_eq!(
1700 render_hit(shaped, true),
1701 serde_json::json!({ "path": "canon" })
1702 );
1703 let mut o = oqx::Object::new();
1705 o.insert(ID_KEY, Value::Str("d_1".into()));
1706 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1707 o.insert("path", Value::Str("/a.md".into()));
1708 assert_eq!(
1709 render_hit(shape_row(Value::Object(o)).unwrap(), false),
1710 serde_json::json!({ "id": "d_1", "path": "/a.md" })
1711 );
1712 }
1713
1714 #[test]
1715 fn bare_needs_a_select_and_is_not_the_values_form() {
1716 let parse = |s: &str| oqx::parse_string(s).unwrap();
1717 for q in ["from docs", "from docs where layer == \"canon\""] {
1718 let e = check_bare(&parse(q)).unwrap_err();
1719 assert_eq!(e.code, "filter_invalid", "{q}");
1720 assert_eq!(e.message, BARE_NEEDS_SELECT_MESSAGE, "{q}");
1721 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1722 }
1723 let e = check_bare(&parse("select path values from docs")).unwrap_err();
1724 assert_eq!(e.code, "filter_invalid");
1725 assert_eq!(e.message, BARE_WITH_VALUES_MESSAGE);
1726 for q in [
1727 "select path from docs",
1728 "select id, title from docs",
1729 "docs first { select t: title }",
1730 ] {
1731 check_bare(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1732 }
1733 }
1734
1735 #[test]
1736 fn a_projection_onto_the_envelope_must_be_aliased() {
1737 let items = |src: &str| oqx::parse_string(src).unwrap().select;
1738 assert!(envelope_collision(&items("select id, path from docs")).is_none());
1740 assert!(
1741 envelope_collision(&items("select id: id, path: path, p: path from docs")).is_none()
1742 );
1743 let e = envelope_collision(&items("select data.path from docs")).unwrap();
1745 assert_eq!(e.code, "filter_invalid");
1746 assert_eq!(
1747 e.message,
1748 "projection `path` collides with the hit envelope — alias it (`p: data.path`)"
1749 );
1750 let e = envelope_collision(&items("select id: layer from docs")).unwrap();
1751 assert_eq!(
1752 e.message,
1753 "projection `id` collides with the hit envelope — alias it (`i: data.id`)"
1754 );
1755 assert!(envelope_collision(&items("select path: doc.path from blocks")).is_some());
1756 assert!(envelope_collision(&items("select path: nodes collect { } from docs")).is_some());
1757 }
1758
1759 #[test]
1760 fn a_flatten_bound_onto_the_envelope_is_refused() {
1761 let flatten = |src: &str| oqx::parse_string(src).unwrap().flatten;
1762 assert!(
1764 flatten_collision(&flatten("select path, t from docs flatten tags as t")).is_none()
1765 );
1766 assert!(
1767 flatten_collision(&flatten(
1768 "select path, b from docs flatten blocks { where type == \"task\" } as b flatten b.children as c"
1769 ))
1770 .is_none()
1771 );
1772 assert!(flatten_collision(&flatten("select path from docs")).is_none());
1773 let e = flatten_collision(&flatten("select path from docs flatten tags as id")).unwrap();
1775 assert_eq!(e.code, "filter_invalid");
1776 assert_eq!(
1777 e.message,
1778 "flatten `as id` collides with the hit envelope — a flattened row is still the row, whose `id` the envelope reads; bind another name"
1779 );
1780 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1781 let e = flatten_collision(&flatten(
1782 "select title from docs flatten tags as t flatten data.paths as path",
1783 ))
1784 .unwrap();
1785 assert_eq!(
1786 e.message,
1787 "flatten `as path` collides with the hit envelope — a flattened row is still the row, whose `path` the envelope reads; bind another name"
1788 );
1789 }
1790
1791 #[test]
1792 fn a_path_read_is_the_attribute_or_a_row_handle_never_a_source_handle() {
1793 let pred = |src: &str| match oqx::parse_string(src).unwrap().r#where.unwrap() {
1794 oqx::ast::Where::Scalar { expr, .. } => expr,
1795 other => panic!("{other:?}"),
1796 };
1797 let left = |e: Expr| match e {
1798 Expr::Binary { left, .. } => *left,
1799 other => panic!("{other:?}"),
1800 };
1801 assert!(is_path_read(&left(pred("from docs where path == \"x\""))));
1802 assert!(is_path_read(&left(pred(
1803 "from blocks where doc.path == \"x\""
1804 ))));
1805 assert!(is_path_read(&left(pred("from docs where ^path == \"x\""))));
1806 assert!(is_path_read(&left(pred(
1807 "from links where dst_path == \"x\""
1808 ))));
1809 assert!(!is_path_read(&left(pred(
1810 "from docs where data.path == \"x\""
1811 ))));
1812 assert!(!is_path_read(&left(pred(
1813 "from docs where frontmatter.path == \"x\""
1814 ))));
1815 }
1816
1817 #[test]
1818 fn a_hit_that_is_not_a_store_row_fails_the_query() {
1819 let mut o = oqx::Object::new();
1822 o.insert(ID_KEY, Value::Undefined);
1823 o.insert(PATH_KEY, Value::Undefined);
1824 o.insert(SELF_KEY, Value::Str("/timeline/kickoff.md".into()));
1825 let e = shape_row(Value::Object(o)).unwrap_err();
1826 assert_eq!(e.code, "filter_invalid");
1827 assert_eq!(
1828 e.message,
1829 "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>)"
1830 );
1831 let mut row = oqx::Object::new();
1833 row.insert("doc_id", Value::Str("d_1".into()));
1834 row.insert("path", Value::Str("a.md".into()));
1835 let row = crate::context::tag_row(row, crate::context::Target::Docs);
1836 let mut o = oqx::Object::new();
1837 o.insert(ID_KEY, Value::Str("d_1".into()));
1838 o.insert(PATH_KEY, Value::Str("a.md".into()));
1839 o.insert(SELF_KEY, row);
1840 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1841 assert_eq!(
1842 hit.as_object().unwrap().keys().collect::<Vec<_>>(),
1843 ["id", "path"]
1844 );
1845 let parse = |s: &str| oqx::parse_string(s).unwrap();
1847 assert!(!may_reach_non_rows(&parse(
1848 "from docs where layer == \"canon\""
1849 )));
1850 assert!(may_reach_non_rows(&parse("from docs follow before")));
1851 assert!(may_reach_non_rows(&parse("refs(\"/index.md\") first { }")));
1852 }
1853
1854 #[test]
1855 fn distinct_dedups_by_user_projection_first_wins() {
1856 let mk = |id: &str, t: &str| {
1857 let mut proj = oqx::Object::new();
1858 proj.insert("type", Value::Str(t.into()));
1859 Shaped {
1860 id: id.to_owned(),
1861 path: "p".to_owned(),
1862 proj,
1863 }
1864 };
1865 let out = dedup_by_projection(vec![mk("1", "a"), mk("2", "b"), mk("3", "a")], false);
1866 assert_eq!(out.len(), 2);
1867 assert_eq!(out[0].id, "1");
1868 assert_eq!(out[1].id, "2");
1869 let with_id = |id: &str| {
1873 let mut proj = oqx::Object::new();
1874 proj.insert("id", Value::Str(id.into()));
1875 Shaped {
1876 id: id.to_owned(),
1877 path: "p".to_owned(),
1878 proj,
1879 }
1880 };
1881 assert_eq!(
1882 dedup_by_projection(vec![with_id("1"), with_id("2")], false).len(),
1883 1
1884 );
1885 assert_eq!(
1886 dedup_by_projection(vec![with_id("1"), with_id("2")], true).len(),
1887 2
1888 );
1889 }
1890
1891 #[test]
1892 fn the_page_runs_on_identity() {
1893 let rows: Vec<(String, String)> = [("/a.md", "d_1"), ("/a.md", "d_2"), ("/b.md", "d_3")]
1894 .iter()
1895 .map(|(p, i)| ((*p).to_owned(), (*i).to_owned()))
1896 .collect();
1897 let ident = |r: &(String, String)| (r.0.clone(), r.1.clone());
1898 let (page, truncated, next) =
1900 page_rows(rows.clone(), ident, 0, None, None, 2, false).unwrap();
1901 assert_eq!(page.len(), 2);
1902 assert!(truncated);
1903 let next = next.expect("a cursor");
1904 assert_eq!(next, encode_cursor(&["/a.md", "d_2"]));
1905 let (page, truncated, next) =
1907 page_rows(rows.clone(), ident, 0, None, Some(&next), 2, false).unwrap();
1908 assert_eq!(page, vec![("/b.md".to_owned(), "d_3".to_owned())]);
1909 assert!(!truncated && next.is_none());
1910 let (page, truncated, next) =
1912 page_rows(rows.clone(), ident, 1, Some(1), None, 50, true).unwrap();
1913 assert_eq!(page, vec![("/a.md".to_owned(), "d_2".to_owned())]);
1914 assert!(!truncated && next.is_none());
1915 let (page, truncated, next) = page_rows(rows, ident, 0, None, None, 1, true).unwrap();
1916 assert_eq!(page.len(), 1);
1917 assert!(truncated && next.is_none());
1918 }
1919
1920 #[test]
1921 fn top_level_bounds_must_be_literals() {
1922 assert_eq!(const_bound(None, "limit").unwrap(), None);
1923 assert_eq!(
1924 const_bound(
1925 Some(&Expr::Lit {
1926 value: Value::Number(3.0),
1927 span: oqx::Span::EMPTY
1928 }),
1929 "limit"
1930 )
1931 .unwrap(),
1932 Some(3)
1933 );
1934 let e = const_bound(
1935 Some(&Expr::Lit {
1936 value: Value::Number(-1.0),
1937 span: oqx::Span::EMPTY,
1938 }),
1939 "offset",
1940 )
1941 .unwrap_err();
1942 assert_eq!(e.code, "filter_invalid");
1943 assert!(e.message.contains("top-level offset"));
1944 }
1945}