1use std::collections::HashMap;
14
15use omgbase_search::{EmbeddingProvider, f32_to_blob};
16use omgbase_store::Store;
17use oqx::ast::{BinaryOp, Expr, Flatten, Query, SelectItem};
18use oqx::walk::{Clause, Node, VisitContext, Visitor, transform, visit};
19use oqx::{Consumer, Engine, InMemoryEngine, Value, build, resolve_aliases};
20use serde_json::{Map, Value as Json};
21
22use crate::context::{SemanticVec, StoreContext, Target, render_row_values, target_of};
23use crate::cursor::{decode_path_cursor, encode_cursor};
24use crate::dates;
25use crate::error::{Result, SurfaceError};
26use crate::names::{UnknownNames, VocabularyCache, check_names};
27use crate::paths::reference_path;
28use crate::planner::{SqlitePlanner, root_target};
29
30pub const DEFAULT_LIMIT: usize = 50;
32
33const ROW_FNS: [&str; 12] = [
40 "text",
41 "semantic",
42 "under",
43 "under_heading",
44 "within",
45 "under_kind",
46 "yaml_path",
47 "json_pointer",
48 "has_edge",
49 "has_anchor",
50 "child_count",
51 "parent_type",
52];
53
54const ID_KEY: &str = "__oqx_id";
55const PATH_KEY: &str = "__oqx_path";
56pub(crate) const HIT_ID: &str = "_id";
62pub(crate) const HIT_PATH: &str = "_path";
63const SELF_KEY: &str = "__oqx_self";
73const VALUE_KEY: &str = "__oqx_value";
77
78#[derive(Clone, Copy, Default)]
80pub struct QueryOptions<'a> {
81 pub limit: Option<usize>,
83 pub cursor: Option<&'a str>,
85 pub provider: Option<&'a dyn EmbeddingProvider>,
88 pub in_memory: bool,
91 pub bare: bool,
99 pub unknown_names: Option<UnknownNames>,
107 pub vocabulary: Option<&'a VocabularyCache>,
110 pub now_ms: Option<i64>,
116 pub request: Option<&'a Json>,
121}
122
123#[derive(Clone, Debug, PartialEq)]
125pub struct OqxResult {
126 pub hits: Vec<Json>,
127 pub truncated: bool,
128 pub cursor: Option<String>,
129 pub consumer: Consumer,
130 pub count: Option<f64>,
131 pub exists: Option<bool>,
132 pub none: Option<bool>,
133 pub values: Option<Vec<Json>>,
135 pub hints: Vec<String>,
141}
142
143impl OqxResult {
144 fn scalar(consumer: Consumer) -> Self {
145 Self {
146 hits: Vec::new(),
147 truncated: false,
148 cursor: None,
149 consumer,
150 count: None,
151 exists: None,
152 none: None,
153 values: None,
154 hints: Vec::new(),
155 }
156 }
157
158 #[must_use]
161 pub fn to_json(&self) -> Json {
162 let mut m = Map::new();
163 m.insert("hits".to_owned(), Json::Array(self.hits.clone()));
164 m.insert("truncated".to_owned(), Json::Bool(self.truncated));
165 m.insert(
166 "cursor".to_owned(),
167 self.cursor.clone().map_or(Json::Null, Json::String),
168 );
169 m.insert(
170 "consumer".to_owned(),
171 Json::String(self.consumer.as_str().to_owned()),
172 );
173 if let Some(n) = self.count {
174 m.insert("count".to_owned(), Value::Number(n).to_canonical_json());
175 }
176 if let Some(b) = self.exists {
177 m.insert("exists".to_owned(), Json::Bool(b));
178 }
179 if let Some(b) = self.none {
180 m.insert("none".to_owned(), Json::Bool(b));
181 }
182 if let Some(v) = &self.values {
183 m.insert("values".to_owned(), Json::Array(v.clone()));
184 }
185 if !self.hints.is_empty() {
186 m.insert(
187 "hints".to_owned(),
188 Json::Array(self.hints.iter().cloned().map(Json::String).collect()),
189 );
190 }
191 Json::Object(m)
192 }
193}
194
195const PATH_ATTRIBUTES: [&str; 2] = ["_path", "_dst_path"];
208const ROW_HANDLES: [&str; 4] = ["_doc", "_block", "_dst", "$it"];
213
214fn is_path_read(e: &Expr) -> bool {
215 match e {
216 Expr::Ident { name, .. } | Expr::Outer { name, .. } => {
217 PATH_ATTRIBUTES.contains(&name.as_str())
218 }
219 Expr::Member { recv, name, .. } if PATH_ATTRIBUTES.contains(&name.as_str()) => {
220 matches!(&**recv, Expr::Ident { name, .. } | Expr::Outer { name, .. } if ROW_HANDLES.contains(&name.as_str()))
221 }
222 _ => false,
223 }
224}
225
226fn envelope_collision(items: &[SelectItem]) -> Option<SurfaceError> {
239 for item in items {
240 let name = match item {
241 SelectItem::Field { name, .. } | SelectItem::Collect { name, .. } => name.as_str(),
242 };
243 if name != HIT_ID && name != HIT_PATH {
244 continue;
245 }
246 if let SelectItem::Field { expr, .. } = item
247 && matches!(expr, Expr::Ident { name: n, .. } if n == name)
248 {
249 continue;
250 }
251 let hint = if name == HIT_PATH { "p" } else { "i" };
252 return Some(SurfaceError::filter_invalid(
253 format!("projection `{name}` collides with the hit envelope — alias it (`{hint}: …`)"),
254 "OQX",
255 ));
256 }
257 None
258}
259
260fn flatten_collision(flatten: &[Flatten]) -> Option<SurfaceError> {
265 let f = flatten
266 .iter()
267 .find(|f| f.name == HIT_ID || f.name == HIT_PATH)?;
268 Some(SurfaceError::filter_invalid(
269 format!(
270 "flatten `as {0}` collides with the hit envelope — a flattened row is still the row, whose `{0}` the envelope reads; bind another name",
271 f.name
272 ),
273 "OQX",
274 ))
275}
276
277fn root_literal(e: Expr) -> Expr {
278 match e {
279 Expr::Lit {
280 value: Value::Str(s),
281 span,
282 } => Expr::Lit {
283 value: Value::Str(reference_path(&s)),
284 span,
285 },
286 other => other,
287 }
288}
289
290#[must_use]
292pub fn root_path_literals(e: Expr) -> Expr {
293 match e {
294 Expr::Binary {
295 op: op @ (BinaryOp::Eq | BinaryOp::Ne),
296 left,
297 right,
298 span,
299 } => {
300 if is_path_read(&left) {
301 Expr::Binary {
302 op,
303 left,
304 right: Box::new(root_literal(*right)),
305 span,
306 }
307 } else if is_path_read(&right) {
308 Expr::Binary {
309 op,
310 left: Box::new(root_literal(*left)),
311 right,
312 span,
313 }
314 } else {
315 Expr::Binary {
316 op,
317 left,
318 right,
319 span,
320 }
321 }
322 }
323 Expr::Call {
324 recv: Some(recv),
325 name,
326 mut args,
327 span,
328 } if name == "startsWith" && args.len() == 1 && is_path_read(&recv) => {
329 let arg = root_literal(args.remove(0));
330 Expr::Call {
331 recv: Some(recv),
332 name,
333 args: vec![arg],
334 span,
335 }
336 }
337 other => other,
338 }
339}
340
341#[must_use]
353pub fn fold_dates(e: Expr, now_ms: i64) -> Expr {
354 let Expr::Call {
355 recv, name, args, ..
356 } = &e
357 else {
358 return e;
359 };
360 let mut vals = Vec::with_capacity(args.len());
361 for a in args {
362 let Expr::Lit { value, .. } = a else { return e };
363 vals.push(value.clone());
364 }
365 let folded = match recv {
366 None => {
367 if dates::date_function_arity(name) != Some(vals.len()) {
368 return e;
369 }
370 dates::date_function(name, &vals, now_ms)
371 }
372 Some(r) => {
373 let Expr::Lit { value, .. } = &**r else {
374 return e;
375 };
376 if dates::date_method_arity(name) != Some(vals.len()) {
377 return e;
378 }
379 dates::date_method(name, value, &vals)
380 }
381 };
382 match folded {
383 Some(Ok(v)) => build::lit(v),
384 _ => e,
385 }
386}
387
388#[must_use]
393pub fn rewrite_query(q: &Query, now_ms: i64) -> Query {
394 transform(q, &mut |e, _| match e {
395 Expr::Call {
396 recv: None,
397 name,
398 args,
399 span,
400 } if ROW_FNS.contains(&name.as_str()) => Expr::Call {
401 recv: Some(Box::new(build::ident("$it"))),
402 name,
403 args,
404 span,
405 },
406 other => fold_dates(root_path_literals(other), now_ms),
407 })
408}
409
410pub 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`";
414
415#[must_use]
419pub fn bare_target_message(t: Target, depth: Option<usize>) -> String {
420 let name = t.as_str();
421 let noun = t.noun();
422 let carets = "^".repeat(depth.unwrap_or(1));
423 let how = if depth.is_none() {
424 format!("; one caret per enclosing block, or the absolute `0^{name}`")
425 } else {
426 String::new()
427 };
428 format!(
429 "`{name}` inside a block reads a property of the current row, which has none — did you mean `{carets}{name}` (the repository's {noun}{how})?"
430 )
431}
432
433#[must_use]
439pub fn unknown_target_message(name: &str) -> String {
440 let was = if name == "edges" {
441 " (`edges` was renamed `links` in surface 3.0)"
442 } else {
443 ""
444 };
445 let roots: Vec<String> = crate::context::ALL_TARGETS
446 .iter()
447 .map(|t| format!("`{}`", t.as_str()))
448 .collect();
449 format!(
450 "`{name}` is not a target — the repository's collections are {}{was}",
451 roots.join(", ")
452 )
453}
454
455#[must_use]
459pub fn past_root_message(t: Target, levels: usize) -> String {
460 let name = t.as_str();
461 let noun = t.noun();
462 let carets = "^".repeat(levels);
463 format!(
464 "`{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})`)"
465 )
466}
467
468pub fn check_root_spellings(q: &Query) -> Result<()> {
482 if let Expr::Ident { name, .. } = &q.source {
486 if Target::parse(name).is_none() {
487 return Err(SurfaceError::filter_invalid(
488 unknown_target_message(name),
489 "OQX",
490 ));
491 }
492 }
493 struct Check(Option<SurfaceError>);
494 impl Visitor<'_> for Check {
495 fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
496 if self.0.is_some() {
497 return false;
498 }
499 match node {
500 Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. })
501 if name == "$repo" =>
502 {
503 self.0 = Some(SurfaceError::filter_invalid(REPO_REMOVED_MESSAGE, "OQX"));
504 }
505 Node::Expr(Expr::Ident { name, .. })
506 if ctx.depth >= 1
507 && let Some(t) = Target::parse(name) =>
508 {
509 self.0 = Some(SurfaceError::filter_invalid(
510 bare_target_message(t, Some(ctx.depth)),
511 "OQX",
512 ));
513 }
514 Node::Expr(Expr::Outer { name, levels, .. }) if *levels > ctx.depth => {
515 if let Some(t) = Target::parse(name) {
516 self.0 = Some(SurfaceError::filter_invalid(
517 past_root_message(t, *levels),
518 "OQX",
519 ));
520 }
521 }
522 _ => {}
523 }
524 self.0.is_none()
525 }
526 }
527 let mut check = Check(None);
528 visit(Node::Query(q), &mut check);
529 check.0.map_or(Ok(()), Err)
530}
531
532struct Phrases(Vec<String>);
536
537impl Visitor<'_> for Phrases {
538 fn enter(&mut self, node: Node<'_>, _ctx: &VisitContext<'_>) -> bool {
539 if let Node::Expr(Expr::Call {
540 recv: None,
541 name,
542 args,
543 ..
544 }) = node
545 && name == "semantic"
546 && let Some(Expr::Lit {
547 value: Value::Str(s),
548 ..
549 }) = args.first()
550 && !self.0.contains(s)
551 {
552 self.0.push(s.clone());
553 }
554 true
555 }
556}
557
558#[must_use]
561pub fn collect_semantic_phrases(source: &str) -> Vec<String> {
562 let Ok(q) = oqx::parse_string(source) else {
563 return Vec::new();
564 };
565 semantic_phrases_of(&q)
566}
567
568#[must_use]
570pub fn semantic_phrases_of(q: &Query) -> Vec<String> {
571 let mut phrases = Phrases(Vec::new());
572 visit(Node::Query(q), &mut phrases);
573 phrases.0
574}
575
576pub fn embed_phrases(
583 provider: Option<&dyn EmbeddingProvider>,
584 phrases: Vec<String>,
585) -> Result<HashMap<String, SemanticVec>> {
586 let mut semantic: HashMap<String, SemanticVec> = HashMap::new();
587 if let Some(provider) = provider.filter(|_| !phrases.is_empty()) {
588 for phrase in phrases {
589 let vec = provider
590 .embed_query(&phrase)
591 .map_err(|e| SurfaceError::new(e.code(), e.to_string()))?;
592 semantic.insert(
593 phrase,
594 SemanticVec {
595 model: provider.model().to_owned(),
596 vec: f32_to_blob(&vec),
597 },
598 );
599 }
600 }
601 Ok(semantic)
602}
603
604pub const RESERVED_ALIAS_PREFIXES: [&str; 2] = ["__oqx_", "__omg_"];
616
617pub fn check_reserved_aliases(q: &Query) -> Result<()> {
619 for item in &q.select {
620 if let Some(prefix) = RESERVED_ALIAS_PREFIXES
621 .iter()
622 .find(|p| item.name().starts_with(*p))
623 {
624 return Err(SurfaceError::filter_invalid(
625 format!(
626 "projection `{}` uses the reserved prefix `{prefix}` — choose another alias",
627 item.name()
628 ),
629 "OQX",
630 ));
631 }
632 }
633 Ok(())
634}
635
636fn mentions_outside_source(q: &Query, name: &str) -> bool {
642 struct Mentions<'n> {
643 name: &'n str,
644 found: bool,
645 }
646 impl Visitor<'_> for Mentions<'_> {
647 fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
648 if self.found {
649 return false;
650 }
651 if ctx.clause == Some(Clause::Source) && ctx.path.len() == 1 {
653 return false;
654 }
655 if let Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. }) = node
656 && name == self.name
657 {
658 self.found = true;
659 }
660 !self.found
661 }
662 }
663 let mut m = Mentions { name, found: false };
664 visit(Node::Query(q), &mut m);
665 m.found
666}
667
668fn may_reach_non_rows(q: &Query) -> bool {
676 q.follow.is_some() || !q.from.is_empty() || root_target(&q.source).is_none()
677}
678
679fn describe_value(v: &Value) -> String {
681 match v {
682 Value::Undefined | Value::Null => "an absent value".to_owned(),
683 Value::Str(s) => format!(
684 "a string ({})",
685 serde_json::to_string(s).unwrap_or_default()
686 ),
687 Value::Number(_) => format!("a number ({v})"),
688 Value::Bool(b) => format!("a boolean ({b})"),
689 Value::Array(_) => "an array".to_owned(),
690 Value::Range(_) => "a range".to_owned(),
691 Value::Object(_) => "an object".to_owned(),
692 }
693}
694
695fn not_a_store_row(v: &Value) -> SurfaceError {
696 SurfaceError::filter_invalid(
697 format!(
698 "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>)",
699 describe_value(v)
700 ),
701 "OQX",
702 )
703}
704
705#[derive(Debug)]
715struct Shaped {
716 id: String,
717 path: String,
718 proj: oqx::Object,
719}
720
721fn path_string(path: &Value) -> String {
722 if path.is_absent() {
723 String::new()
724 } else {
725 path.to_string()
726 }
727}
728
729fn shape_row(row: Value) -> Result<Shaped> {
730 let mut shaped = Shaped {
731 id: Value::Undefined.to_string(),
732 path: String::new(),
733 proj: oqx::Object::new(),
734 };
735 let Value::Object(mut o) = row else {
736 return Ok(shaped);
737 };
738 if let Some(me) = o.remove(SELF_KEY)
739 && target_of(&me).is_none()
740 {
741 return Err(not_a_store_row(&me));
742 }
743 let Value::Object(o) = render_row_values(Value::Object(o)) else {
744 return Ok(shaped);
745 };
746 for (k, v) in o {
747 match k.as_str() {
748 ID_KEY => shaped.id = v.to_string(),
749 PATH_KEY => shaped.path = path_string(&v),
750 _ => {
751 shaped.proj.insert(k, v);
752 }
753 }
754 }
755 Ok(shaped)
756}
757
758fn raw_identity(row: &Value) -> (String, String) {
763 let get = |k: &str| {
764 row.as_object()
765 .and_then(|o| o.get(k))
766 .cloned()
767 .unwrap_or(Value::Undefined)
768 };
769 (path_string(&get(PATH_KEY)), get(ID_KEY).to_string())
770}
771
772fn to_hit(s: Shaped) -> Value {
777 let mut hit = oqx::Object::with_capacity(s.proj.len() + 2);
778 hit.insert(HIT_ID, Value::Str(s.id));
779 hit.insert(HIT_PATH, Value::Str(s.path));
780 for (k, v) in s.proj {
781 hit.insert(k, v);
782 }
783 Value::Object(hit)
784}
785
786fn to_bare_hit(s: Shaped) -> Value {
791 Value::Object(s.proj)
792}
793
794fn render_hit(s: Shaped, bare: bool) -> Json {
796 if bare {
797 to_bare_hit(s).to_canonical_json()
798 } else {
799 to_hit(s).to_canonical_json()
800 }
801}
802
803fn dedup_by_projection(rows: Vec<Shaped>, bare: bool) -> Vec<Shaped> {
812 let mut seen: Vec<String> = Vec::new();
813 let mut out = Vec::new();
814 for s in rows {
815 let mut pairs: Vec<(String, Value)> = s
816 .proj
817 .iter()
818 .filter(|(k, _)| bare || (*k != HIT_ID && *k != HIT_PATH))
819 .map(|(k, v)| (k.to_owned(), v.clone()))
820 .collect();
821 pairs.sort_by(|a, b| a.0.cmp(&b.0));
822 let key = Value::Array(
823 pairs
824 .into_iter()
825 .map(|(k, v)| Value::Array(vec![Value::Str(k), v]))
826 .collect(),
827 )
828 .to_canonical_json()
829 .to_string();
830 if seen.contains(&key) {
831 continue;
832 }
833 seen.push(key);
834 out.push(s);
835 }
836 out
837}
838
839fn const_bound(e: Option<&Expr>, word: &str) -> Result<Option<usize>> {
842 match e {
843 None => Ok(None),
844 Some(Expr::Lit {
845 value: Value::Number(n),
846 ..
847 }) if n.fract() == 0.0 && *n >= 0.0 && n.is_finite() => Ok(Some(*n as usize)),
848 Some(_) => Err(SurfaceError::filter_invalid(
849 format!("top-level {word} must be a non-negative integer literal"),
850 "OQX",
851 )),
852 }
853}
854
855fn page_rows<T>(
861 mut rows: Vec<T>,
862 ident: impl Fn(&T) -> (String, String),
863 offset: usize,
864 limit: Option<usize>,
865 cursor: Option<&str>,
866 cap: usize,
867 custom: bool,
868) -> Result<(Vec<T>, bool, Option<String>)> {
869 if offset > 0 || limit.is_some() {
870 let end = limit.map_or(rows.len(), |l| (offset + l).min(rows.len()));
871 if offset >= rows.len() {
872 rows.clear();
873 } else {
874 rows.truncate(end);
875 rows.drain(..offset);
876 }
877 }
878 if let Some(cursor) = cursor {
879 let parts = decode_path_cursor(cursor, "query", 2)?;
881 let (path, id) = (&parts[0], &parts[1]);
882 rows.retain(|r| {
883 let (rp, ri) = ident(r);
884 rp > *path || (rp == *path && ri > *id)
885 });
886 }
887 let truncated = rows.len() > cap;
888 rows.truncate(cap);
889 let next = if truncated && !custom {
890 rows.last().map(|last| {
891 let (p, i) = ident(last);
892 encode_cursor(&[&p, &i])
893 })
894 } else {
895 None
896 };
897 Ok((rows, truncated, next))
898}
899
900fn value_of(s: &Shaped) -> Json {
901 s.proj
902 .get(VALUE_KEY)
903 .cloned()
904 .unwrap_or(Value::Undefined)
905 .to_canonical_json()
906}
907
908pub fn check_bare(q: &Query) -> Result<()> {
915 if q.values {
916 return Err(SurfaceError::filter_invalid(
917 BARE_WITH_VALUES_MESSAGE,
918 "OQX",
919 ));
920 }
921 if q.select.is_empty() {
922 return Err(SurfaceError::filter_invalid(
923 BARE_NEEDS_SELECT_MESSAGE,
924 "OQX",
925 ));
926 }
927 Ok(())
928}
929
930pub const BARE_NEEDS_SELECT_MESSAGE: &str =
932 "`bare` needs a `select` — without a projection a hit is only its envelope";
933pub const BARE_WITH_VALUES_MESSAGE: &str =
934 "`bare` is redundant with `values` — `values` is already the bare-values form; drop one";
935
936pub fn query(
940 store: &Store,
941 repo_id: &str,
942 source: &str,
943 opts: QueryOptions<'_>,
944) -> Result<OqxResult> {
945 let semantic = embed_phrases(opts.provider, collect_semantic_phrases(source))?;
946 let runner = Runner {
947 store,
948 repo_id,
949 semantic,
950 planned: !opts.in_memory,
951 now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
952 };
953 let raw = oqx::parse_string(source).map_err(|e| parse_invalid(source, &e, opts.request))?;
954 check_reserved_aliases(&raw)?;
955 run_parsed(
957 &runner,
958 &raw,
959 opts,
960 opts.unknown_names.unwrap_or(UnknownNames::Hint),
961 )
962}
963
964const BRACKET_ERROR_HEAD: &str = "unexpected '[' — expected a value (at offset ";
977pub const TUPLE_HINT: &str = "; a list is written as a tuple: (…)";
979
980fn bracket_error_offset(message: &str) -> Option<usize> {
983 let digits = message
984 .strip_prefix(BRACKET_ERROR_HEAD)?
985 .strip_suffix(')')?;
986 if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
987 return None;
988 }
989 digits.parse().ok()
990}
991
992#[must_use]
996pub fn bracket_list_as_tuple(source: &str, message: &str) -> Option<String> {
997 let open = bracket_error_offset(message)?;
998 let mut cps: Vec<char> = source.chars().collect();
999 if cps.get(open) != Some(&'[') {
1000 return None;
1001 }
1002 let mut i = open + 1;
1003 let mut members = 0;
1004 let close = loop {
1005 i = skip_spaces(&cps, i);
1006 let end = literal_end(&cps, i)?;
1007 members += 1;
1008 i = skip_spaces(&cps, end);
1009 match cps.get(i) {
1010 Some(',') => i += 1,
1011 Some(']') => break i,
1012 _ => return None,
1013 }
1014 };
1015 if members < 2 {
1016 return None;
1017 }
1018 cps[open] = '(';
1019 cps[close] = ')';
1020 Some(cps.into_iter().collect())
1021}
1022
1023fn skip_spaces(cps: &[char], mut i: usize) -> usize {
1024 while i < cps.len() && cps[i].is_whitespace() {
1025 i += 1;
1026 }
1027 i
1028}
1029
1030fn literal_end(cps: &[char], i: usize) -> Option<usize> {
1034 let c = *cps.get(i)?;
1035 if c == '"' || c == '\'' {
1036 let mut j = i + 1;
1037 while j < cps.len() {
1038 if cps[j] == '\\' {
1039 j += 2;
1040 } else if cps[j] == c {
1041 return Some(j + 1);
1042 } else {
1043 j += 1;
1044 }
1045 }
1046 return None;
1047 }
1048 if c.is_ascii_digit() {
1049 let digits = |mut j: usize| {
1050 while j < cps.len() && cps[j].is_ascii_digit() {
1051 j += 1;
1052 }
1053 j
1054 };
1055 let mut j = digits(i);
1056 if cps.get(j) == Some(&'.') && cps.get(j + 1).is_some_and(char::is_ascii_digit) {
1057 j = digits(j + 1);
1058 }
1059 if matches!(cps.get(j), Some('e' | 'E')) {
1060 let mut k = j + 1;
1061 if matches!(cps.get(k), Some('+' | '-')) {
1062 k += 1;
1063 }
1064 if cps.get(k).is_some_and(char::is_ascii_digit) {
1065 j = digits(k);
1066 }
1067 }
1068 return Some(j);
1069 }
1070 for word in ["true", "false", "null"] {
1071 let n = word.len();
1072 let matches_word = cps.len() >= i + n && cps[i..i + n].iter().copied().eq(word.chars());
1073 let boundary = !cps
1074 .get(i + n)
1075 .is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '$');
1076 if matches_word && boundary {
1077 return Some(i + n);
1078 }
1079 }
1080 None
1081}
1082
1083fn parse_invalid(source: &str, e: &oqx::OqxError, request: Option<&Json>) -> SurfaceError {
1087 let Some(rewritten) = bracket_list_as_tuple(source, &e.message) else {
1088 return SurfaceError::filter_invalid(e.message.clone(), "OQX");
1089 };
1090 let mut err = SurfaceError::filter_invalid(format!("{}{TUPLE_HINT}", e.message), "OQX");
1091 if let Some(Json::Object(args)) = request
1092 && let Some(d) = err.data.as_mut().and_then(Json::as_object_mut)
1093 {
1094 let mut fixed = args.clone();
1095 fixed.insert("query".to_owned(), Json::String(rewritten));
1096 d.insert(
1097 "fix".to_owned(),
1098 Json::String(omgbase_mutate::canonical_json(&Json::Object(fixed))),
1099 );
1100 }
1101 err
1102}
1103
1104pub fn query_ast(
1113 store: &Store,
1114 repo_id: &str,
1115 raw: &Query,
1116 opts: QueryOptions<'_>,
1117 semantic: HashMap<String, SemanticVec>,
1118) -> Result<OqxResult> {
1119 let runner = Runner {
1120 store,
1121 repo_id,
1122 semantic,
1123 planned: !opts.in_memory,
1124 now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
1125 };
1126 run_parsed(
1127 &runner,
1128 raw,
1129 opts,
1130 opts.unknown_names.unwrap_or(UnknownNames::Refuse),
1131 )
1132}
1133
1134struct Runner<'a> {
1140 store: &'a Store,
1141 repo_id: &'a str,
1142 semantic: HashMap<String, SemanticVec>,
1143 planned: bool,
1144 now_ms: i64,
1146}
1147
1148impl Runner<'_> {
1149 fn run(&self, q: &Query) -> Result<oqx::OqxResult> {
1156 let conn = self.store.conn();
1157 let ctx =
1158 StoreContext::new(conn, self.repo_id, self.semantic.clone()).with_now_ms(self.now_ms);
1159 let plan = if self.planned {
1160 SqlitePlanner::new(conn, self.repo_id)
1161 .try_plan(q, &[])
1162 .map_err(|e| SurfaceError::other(format!("sqlite: {e}")))?
1163 } else {
1164 None
1165 };
1166 let (ctx, residual) = match plan {
1167 Some(plan) => {
1172 let once = !mentions_outside_source(&plan.residual, oqx::ROWS_ROOT);
1173 let ctx = if once {
1174 ctx.with_rows_root_once(plan.rows)
1175 } else {
1176 ctx.with_rows_root(plan.rows)
1177 };
1178 (ctx, Some(plan.residual))
1179 }
1180 None => (ctx, None),
1181 };
1182 let engine = InMemoryEngine::new(ctx);
1183 let out = engine.run(residual.as_ref().unwrap_or(q), &[]);
1184 if let Some(failed) = engine.context().take_root_failure() {
1187 return Err(failed.into());
1188 }
1189 Ok(out?)
1192 }
1193}
1194
1195fn run_parsed(
1196 engine: &Runner<'_>,
1197 raw: &Query,
1198 opts: QueryOptions<'_>,
1199 mode: UnknownNames,
1200) -> Result<OqxResult> {
1201 check_root_spellings(raw)?;
1202 let own = Query {
1210 select: raw
1211 .select
1212 .iter()
1213 .filter(|it| {
1214 !RESERVED_ALIAS_PREFIXES
1215 .iter()
1216 .any(|p| it.name().starts_with(p))
1217 })
1218 .cloned()
1219 .collect(),
1220 ..raw.clone()
1221 };
1222 let local = VocabularyCache::default();
1223 let cache = opts.vocabulary.unwrap_or(&local);
1224 let hints = check_names(engine.store, engine.repo_id, &own, mode, cache)?;
1225 let mut result = run_resolved(engine, raw, opts)?;
1226 result.hints = hints;
1228 Ok(result)
1229}
1230
1231fn run_resolved(engine: &Runner<'_>, raw: &Query, opts: QueryOptions<'_>) -> Result<OqxResult> {
1232 let parsed = rewrite_query(&resolve_aliases(raw)?, engine.now_ms);
1235 let consumer = parsed.consumer;
1236
1237 match consumer {
1238 Consumer::Exists => {
1239 let res = engine.run(&parsed)?;
1240 let mut r = OqxResult::scalar(consumer);
1241 r.exists = Some(matches!(res, oqx::OqxResult::Exists(true)));
1242 return Ok(r);
1243 }
1244 Consumer::Count => {
1245 let res = engine.run(&parsed)?;
1246 let mut r = OqxResult::scalar(consumer);
1247 r.count = Some(match res {
1248 oqx::OqxResult::Count(n) => n,
1249 _ => 0.0,
1250 });
1251 return Ok(r);
1252 }
1253 Consumer::None => {
1254 let res = engine.run(&parsed)?;
1255 let mut r = OqxResult::scalar(consumer);
1256 r.none = Some(match res {
1257 oqx::OqxResult::None(b) => b,
1258 _ => true,
1259 });
1260 return Ok(r);
1261 }
1262 Consumer::Collect | Consumer::First | Consumer::Single => {}
1263 }
1264
1265 if parsed.group_by.is_some() {
1277 return run_grouped(engine, &parsed, consumer, opts);
1278 }
1279
1280 let flattened = !parsed.flatten.is_empty();
1291 if let Some(e) = flatten_collision(&parsed.flatten) {
1292 return Err(e);
1293 }
1294
1295 let top_distinct = parsed.distinct;
1301 let top_values = parsed.values;
1302 let user_select: Vec<SelectItem> = if top_values {
1303 parsed
1304 .select
1305 .first()
1306 .map(|it| match it {
1307 SelectItem::Field {
1308 expr, lift, span, ..
1309 } => SelectItem::Field {
1310 name: VALUE_KEY.to_owned(),
1311 expr: expr.clone(),
1312 lift: *lift,
1313 span: *span,
1314 },
1315 SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1316 name: VALUE_KEY.to_owned(),
1317 op: op.clone(),
1318 span: *span,
1319 },
1320 })
1321 .into_iter()
1322 .collect()
1323 } else {
1324 parsed.select.clone()
1325 };
1326 let bare = opts.bare;
1330 if bare {
1331 check_bare(&parsed)?;
1332 } else if !top_values && let Some(e) = envelope_collision(&user_select) {
1333 return Err(e);
1334 }
1335 let id_item = build::field(ID_KEY, build::ident(HIT_ID));
1336 let path_item = build::field(PATH_KEY, build::ident(HIT_PATH));
1337 let mut select = vec![id_item, path_item];
1338 if !top_values && may_reach_non_rows(&parsed) {
1339 select.push(build::field(SELF_KEY, build::ident("$it")));
1340 }
1341 select.extend(user_select);
1342 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1346 let q = Query {
1347 distinct: false,
1348 values: false,
1349 select,
1350 limit: if consumer == Consumer::Collect {
1351 None
1352 } else {
1353 parsed.limit.clone()
1354 },
1355 offset: if consumer == Consumer::Collect {
1356 None
1357 } else {
1358 parsed.offset.clone()
1359 },
1360 ..parsed.clone()
1361 };
1362 let res = engine.run(&q)?;
1363
1364 if matches!(consumer, Consumer::First | Consumer::Single) {
1365 let row = match res {
1366 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1367 _ => None,
1368 };
1369 let mut out = OqxResult::scalar(consumer);
1370 match row {
1371 None => {
1372 if top_values {
1373 out.values = Some(Vec::new());
1374 }
1375 }
1376 Some(r) => {
1377 let shaped = shape_row(r)?;
1378 if top_values {
1379 out.values = Some(vec![value_of(&shaped)]);
1380 } else {
1381 out.hits = vec![render_hit(shaped, bare)];
1382 }
1383 }
1384 }
1385 return Ok(out);
1386 }
1387
1388 let rows: Vec<Value> = match res {
1390 oqx::OqxResult::Collect(rows) => rows,
1391 _ => Vec::new(),
1392 };
1393 let custom = parsed.order_by.as_ref().is_some_and(|o| !o.is_empty()) || flattened;
1394 let cursor = opts.cursor.filter(|c| !c.is_empty() && !custom);
1395 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1396 let limit = const_bound(top_limit.as_ref(), "limit")?;
1397 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1398 let (page, truncated, next) = if top_distinct {
1404 let shaped = rows
1405 .into_iter()
1406 .map(shape_row)
1407 .collect::<Result<Vec<_>>>()?;
1408 let shaped = dedup_by_projection(shaped, bare);
1409 page_rows(
1410 shaped,
1411 |s| (s.path.clone(), s.id.clone()),
1412 offset,
1413 limit,
1414 cursor,
1415 cap,
1416 custom,
1417 )?
1418 } else {
1419 let (page, truncated, next) =
1420 page_rows(rows, raw_identity, offset, limit, cursor, cap, custom)?;
1421 let page = page
1422 .into_iter()
1423 .map(shape_row)
1424 .collect::<Result<Vec<_>>>()?;
1425 (page, truncated, next)
1426 };
1427 let mut out = OqxResult::scalar(Consumer::Collect);
1428 out.truncated = truncated;
1429 out.cursor = next;
1430 if top_values {
1431 out.values = Some(page.iter().map(value_of).collect());
1432 } else {
1433 out.hits = page.into_iter().map(|s| render_hit(s, bare)).collect();
1434 }
1435 Ok(out)
1436}
1437
1438fn run_grouped(
1440 engine: &Runner<'_>,
1441 parsed: &Query,
1442 consumer: Consumer,
1443 opts: QueryOptions<'_>,
1444) -> Result<OqxResult> {
1445 let top_values = parsed.values;
1446 let select: Vec<SelectItem> = if top_values {
1449 parsed
1450 .select
1451 .first()
1452 .map(|it| match it {
1453 SelectItem::Field {
1454 expr, lift, span, ..
1455 } => SelectItem::Field {
1456 name: VALUE_KEY.to_owned(),
1457 expr: expr.clone(),
1458 lift: *lift,
1459 span: *span,
1460 },
1461 SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1462 name: VALUE_KEY.to_owned(),
1463 op: op.clone(),
1464 span: *span,
1465 },
1466 })
1467 .into_iter()
1468 .collect()
1469 } else {
1470 parsed.select.clone()
1471 };
1472 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1473 let q = Query {
1474 values: false,
1475 select,
1476 limit: if consumer == Consumer::Collect {
1477 None
1478 } else {
1479 parsed.limit.clone()
1480 },
1481 offset: if consumer == Consumer::Collect {
1482 None
1483 } else {
1484 parsed.offset.clone()
1485 },
1486 ..parsed.clone()
1487 };
1488 let res = engine.run(&q)?;
1489 let project = |row: Value| -> oqx::Object {
1490 match render_row_values(row) {
1491 Value::Object(o) => o,
1492 _ => oqx::Object::new(),
1493 }
1494 };
1495 let value_of = |g: &oqx::Object| -> Json {
1496 g.get(VALUE_KEY)
1497 .cloned()
1498 .unwrap_or(Value::Undefined)
1499 .to_canonical_json()
1500 };
1501 if matches!(consumer, Consumer::First | Consumer::Single) {
1502 let row = match res {
1503 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1504 _ => None,
1505 };
1506 let mut out = OqxResult::scalar(consumer);
1507 match row {
1508 None => {
1509 if top_values {
1510 out.values = Some(Vec::new());
1511 }
1512 }
1513 Some(r) => {
1514 let g = project(r);
1515 if top_values {
1516 out.values = Some(vec![value_of(&g)]);
1517 } else {
1518 out.hits = vec![Value::Object(g).to_canonical_json()];
1519 }
1520 }
1521 }
1522 return Ok(out);
1523 }
1524 let mut groups: Vec<oqx::Object> = match res {
1525 oqx::OqxResult::Collect(rows) => rows.into_iter().map(project).collect(),
1526 _ => Vec::new(),
1527 };
1528 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1529 let limit = const_bound(top_limit.as_ref(), "limit")?;
1530 if offset > 0 || limit.is_some() {
1531 let end = limit.map_or(groups.len(), |l| (offset + l).min(groups.len()));
1532 if offset >= groups.len() {
1533 groups.clear();
1534 } else {
1535 groups.truncate(end);
1536 groups.drain(..offset);
1537 }
1538 }
1539 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1540 let truncated = groups.len() > cap;
1541 groups.truncate(cap);
1542 let mut out = OqxResult::scalar(Consumer::Collect);
1543 out.truncated = truncated;
1544 if top_values {
1545 out.values = Some(groups.iter().map(value_of).collect());
1546 } else {
1547 out.hits = groups
1548 .into_iter()
1549 .map(|g| Value::Object(g).to_canonical_json())
1550 .collect();
1551 }
1552 Ok(out)
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557 use super::*;
1558 use oqx::ast::Where;
1559 use serde_json::json;
1560
1561 fn parse_err(src: &str) -> String {
1564 oqx::parse_string(src).unwrap_err().message
1565 }
1566
1567 #[test]
1568 fn a_bracket_list_of_literals_is_rewritten_to_a_tuple() {
1569 let cases = [
1570 (
1571 "from docs where stage in [\"nigredo\", \"albedo\"]",
1572 "from docs where stage in (\"nigredo\", \"albedo\")",
1573 ),
1574 (
1575 "from blocks where _depth in [0, 1] and _path == \"/index.md\"",
1576 "from blocks where _depth in (0, 1) and _path == \"/index.md\"",
1577 ),
1578 (
1579 "from docs where verified in [ true, 'yes', null ]",
1580 "from docs where verified in ( true, 'yes', null )",
1581 ),
1582 (
1583 "from docs where x in [1.5e3,2E-1,\"a\\\"b\"]",
1584 "from docs where x in (1.5e3,2E-1,\"a\\\"b\")",
1585 ),
1586 (
1588 "from docs where _title in [\"é\", \"ü\"] and stage in [\"a\", \"b\"]",
1589 "from docs where _title in (\"é\", \"ü\") and stage in [\"a\", \"b\"]",
1590 ),
1591 ];
1592 for (src, want) in cases {
1593 let msg = parse_err(src);
1594 assert!(msg.starts_with(BRACKET_ERROR_HEAD), "{src}: {msg}");
1595 assert_eq!(
1596 bracket_list_as_tuple(src, &msg).as_deref(),
1597 Some(want),
1598 "{src}"
1599 );
1600 }
1601 }
1602
1603 #[test]
1604 fn a_bracket_list_that_is_not_two_or_more_literals_has_no_rewrite() {
1605 for src in [
1606 "from docs where stage in [[\"nigredo\"], \"albedo\"]",
1607 "from docs where stage in [stage, \"albedo\"]",
1608 "from docs where stage in [\"nigredo\"]",
1609 "from docs where stage in [\"nigredo\", \"albedo\"",
1610 "from docs where stage in [\"nigredo\" \"albedo\"]",
1611 "from docs where stage in [truex, 1]",
1612 "from docs where stage in [-1, 2]",
1613 "from docs where stage in []",
1614 ] {
1615 let msg = parse_err(src);
1616 assert_eq!(bracket_list_as_tuple(src, &msg), None, "{src}: {msg}");
1617 }
1618 assert_eq!(bracket_list_as_tuple("from docs", "unexpected end"), None);
1620 assert_eq!(
1621 bracket_list_as_tuple(
1622 "from docs",
1623 "unexpected '[' — expected a value (at offset 2)"
1624 ),
1625 None
1626 );
1627 assert_eq!(
1628 bracket_error_offset("unexpected '[' — expected a value (at offset x)"),
1629 None
1630 );
1631 assert_eq!(
1632 bracket_error_offset("unexpected '[' — expected a value (at offset 7)"),
1633 Some(7)
1634 );
1635 }
1636
1637 #[test]
1638 fn a_parse_error_carries_the_hint_and_the_corrected_call() {
1639 let src = "select _path from docs where stage in [\"nigredo\", \"albedo\"]";
1640 let e = oqx::parse_string(src).unwrap_err();
1641 let request = json!({ "query": src, "limit": 5, "bare": true });
1642 let err = parse_invalid(src, &e, Some(&request));
1643 assert_eq!(err.code, "filter_invalid");
1644 assert_eq!(
1645 err.message,
1646 "unexpected '[' — expected a value (at offset 38); a list is written as a tuple: (…)"
1647 );
1648 let d = err.data.unwrap();
1649 assert_eq!(d["hint"], "OQX");
1650 assert_eq!(d["reason"], err.message);
1651 assert_eq!(
1652 d["fix"],
1653 r#"{"bare":true,"limit":5,"query":"select _path from docs where stage in (\"nigredo\", \"albedo\")"}"#
1654 );
1655 let err = parse_invalid(src, &e, None);
1657 assert!(err.message.ends_with(TUPLE_HINT));
1658 assert!(err.data.unwrap().get("fix").is_none());
1659 let src = "from docs where stage in [\"nigredo\"]";
1661 let e = oqx::parse_string(src).unwrap_err();
1662 let err = parse_invalid(src, &e, Some(&json!({ "query": src })));
1663 assert_eq!(
1664 err.message,
1665 "unexpected '[' — expected a value (at offset 25)"
1666 );
1667 assert!(err.data.unwrap().get("fix").is_none());
1668 }
1669
1670 #[test]
1671 fn row_functions_become_self_methods() {
1672 let q = oqx::parse_string(
1673 "from blocks where text(\"x\") && under_heading(\"h\") && size(_attrs) > 0 && _doc._path.startsWith(\"a\")",
1674 )
1675 .unwrap();
1676 let r = rewrite_query(&q, 0);
1677 let Some(Where::And { parts, .. }) = &r.r#where else {
1678 panic!("and")
1679 };
1680 let Where::Scalar { expr, .. } = &parts[0] else {
1681 panic!("scalar")
1682 };
1683 assert!(
1684 matches!(expr, Expr::Call { recv: Some(r), name, .. } if name == "text" && **r == build::ident("$it"))
1685 );
1686 let Where::Scalar { expr, .. } = &parts[2] else {
1687 panic!("scalar")
1688 };
1689 assert!(
1690 matches!(expr, Expr::Binary { left, .. } if matches!(&**left, Expr::Call { recv: None, name, .. } if name == "size"))
1691 );
1692 }
1693
1694 const NOW: i64 = 1_790_467_200_000;
1698
1699 fn where_expr(src: &str) -> Expr {
1700 let q = oqx::parse_string(&format!("from docs where {src}")).unwrap();
1701 let Some(Where::Scalar { expr, .. }) = q.r#where else {
1702 panic!("not a scalar where")
1703 };
1704 expr
1705 }
1706
1707 fn fold(src: &str) -> Expr {
1709 fn go(e: Expr) -> Expr {
1710 match e {
1711 Expr::Call {
1712 recv,
1713 name,
1714 args,
1715 span,
1716 } => fold_dates(
1717 Expr::Call {
1718 recv: recv.map(|r| Box::new(go(*r))),
1719 name,
1720 args: args.into_iter().map(go).collect(),
1721 span,
1722 },
1723 NOW,
1724 ),
1725 other => other,
1726 }
1727 }
1728 go(where_expr(src))
1729 }
1730
1731 fn lit(e: &Expr) -> Option<&Value> {
1732 match e {
1733 Expr::Lit { value, .. } => Some(value),
1734 _ => None,
1735 }
1736 }
1737
1738 #[test]
1739 fn folds_date_dur_over_literals_and_date_methods_over_literal_receivers_and_arguments() {
1740 assert_eq!(
1741 lit(&fold("date(\"2026-01-05\")")),
1742 Some(&Value::from("2026-01-05"))
1743 );
1744 assert_eq!(
1745 lit(&fold("date(\"today\")")),
1746 Some(&Value::from("2026-09-27"))
1747 );
1748 assert_eq!(
1749 lit(&fold("dur(\"30 days\")")),
1750 Some(&Value::from(30.0 * 86400.0))
1751 );
1752 assert_eq!(
1753 lit(&fold("date(\"today\").minus(dur(\"30 days\"))")),
1754 Some(&Value::from("2026-08-28"))
1755 );
1756 assert_eq!(lit(&fold("date(\"x\")")), Some(&Value::Null));
1757 assert_eq!(lit(&fold("date(5)")), Some(&Value::Null));
1758 assert_eq!(lit(&fold("dur(60).dur_text()")), Some(&Value::from("1m")));
1759 }
1760
1761 #[test]
1762 fn leaves_a_non_literal_argument_an_unknown_name_and_a_wrong_arity_in_place() {
1763 for src in [
1764 "date(when)",
1765 "date(\"today\").until(when)",
1766 "size(\"x\")",
1767 "date()",
1768 "date(\"2026-01-01\").plus()",
1769 "\"x\".lower()",
1770 ] {
1771 assert!(matches!(fold(src), Expr::Call { .. }), "{src}");
1772 }
1773 }
1774
1775 #[test]
1776 fn the_runner_folds_at_its_clock_so_a_named_point_pushes_down() {
1777 let q = oqx::parse_string("from docs where _updated_at < date(\"today\") && date(when) < date(\"today\").minus(dur(\"30 days\"))").unwrap();
1778 let printed = oqx::print_query(&rewrite_query(&q, NOW)).unwrap();
1779 assert!(
1780 printed.contains("_updated_at < \"2026-09-27\""),
1781 "{printed}"
1782 );
1783 assert!(printed.contains("date(when) < \"2026-08-28\""), "{printed}");
1784 }
1785
1786 #[test]
1787 fn path_literals_are_rooted_only_against_path_reads() {
1788 let q = oqx::parse_string(
1789 "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 == \"c.md\" && ^_path == \"b.md\"",
1790 )
1791 .unwrap();
1792 let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1793 assert!(printed.contains("_path.startsWith(\"/lab/\")"), "{printed}");
1794 assert!(printed.contains("_doc._path == \"/a.md\""), "{printed}");
1795 assert!(printed.contains("type == \"a.md\""), "{printed}");
1796 assert!(printed.contains("^_path == \"/b.md\""), "{printed}");
1797 assert!(printed.contains("\"/x.md\" != _dst_path"), "{printed}");
1798 assert!(printed.contains("_path == ^after"), "{printed}");
1801 assert!(printed.contains("path == \"c.md\""), "{printed}");
1802 let q = oqx::parse_string(
1804 "from docs where _path.contains(\"a/\") && _path.endsWith(\".md\") && _path == \"\"",
1805 )
1806 .unwrap();
1807 let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1808 assert!(printed.contains("_path.contains(\"a/\")"), "{printed}");
1809 assert!(printed.contains("_path.endsWith(\".md\")"), "{printed}");
1810 assert!(printed.contains("_path == \"/\""), "{printed}");
1811 }
1812
1813 #[test]
1814 fn repo_and_bare_targets_are_refused_statically() {
1815 let parse = |s: &str| oqx::parse_string(s).unwrap();
1816 for q in [
1817 "from docs where $repo",
1818 "select r: size($repo.docs) from docs",
1819 "select r: $repo._id from docs",
1820 "select r: _nodes first { select x: ^$repo._id values } from docs",
1821 "select r: _nodes first { select x: 0^$repo values } from docs",
1822 "$repo.docs count { }",
1823 ] {
1824 let e = check_root_spellings(&parse(q)).unwrap_err();
1825 assert_eq!(e.code, "filter_invalid", "{q}");
1826 assert_eq!(e.message, REPO_REMOVED_MESSAGE, "{q}");
1827 }
1828 let e = check_root_spellings(&parse("select x: docs collect { } from docs")).unwrap_err();
1831 assert_eq!(e.message, bare_target_message(Target::Docs, Some(1)));
1832 assert!(
1833 e.message
1834 .contains("did you mean `^docs` (the repository's documents)?")
1835 );
1836 let e = check_root_spellings(&parse(
1837 "select x: _nodes collect { select y: size(links) } from docs",
1838 ))
1839 .unwrap_err();
1840 assert!(
1841 e.message
1842 .contains("did you mean `^^links` (the repository's links)?")
1843 );
1844 let e = check_root_spellings(&parse("docs collect { from docs }")).unwrap_err();
1845 assert!(e.message.contains("`^docs`"));
1846 let e = check_root_spellings(&parse("from docs where nodes exists { }")).unwrap_err();
1849 assert_eq!(e.message, bare_target_message(Target::Nodes, Some(1)));
1850 let e = check_root_spellings(&parse("from blocks where blocks exists { }")).unwrap_err();
1851 assert_eq!(e.message, bare_target_message(Target::Blocks, Some(1)));
1852 let e =
1853 check_root_spellings(&parse("select r: revisions count { } from docs")).unwrap_err();
1854 assert_eq!(e.message, bare_target_message(Target::Revisions, Some(1)));
1855 for q in [
1857 "docs count { }",
1858 "from docs where _nodes exists { }",
1859 "entries(docs) first { }",
1860 "select n: size(^docs), i: ^_id, k: size(0^links) from docs",
1861 "from blocks where _nodes exists { where ^blocks exists { } }",
1862 ] {
1863 check_root_spellings(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1864 }
1865 let e = check_root_spellings(&parse("^docs count { }")).unwrap_err();
1867 assert_eq!(e.message, past_root_message(Target::Docs, 1));
1868 assert_eq!(
1869 e.message,
1870 "`^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)`)"
1871 );
1872 let e = check_root_spellings(&parse("select _path from docs where ^^links exists { }"))
1873 .unwrap_err();
1874 assert!(
1875 e.message.contains("`^^links` reaches past the root"),
1876 "{}",
1877 e.message
1878 );
1879 let m = bare_target_message(Target::Blocks, None);
1881 assert!(m.contains("did you mean `^blocks` (the repository's blocks; one caret per enclosing block, or the absolute `0^blocks`)?"), "{m}");
1882 }
1883
1884 #[test]
1885 fn reserved_projection_names_are_refused() {
1886 let parse = |s: &str| oqx::parse_string(s).unwrap();
1887 let e = check_reserved_aliases(&parse("select __oqx_id: 1 from docs")).unwrap_err();
1888 assert_eq!(e.code, "filter_invalid");
1889 assert_eq!(
1890 e.message,
1891 "projection `__oqx_id` uses the reserved prefix `__oqx_` — choose another alias"
1892 );
1893 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1894 let e = check_reserved_aliases(&parse("select __omg_sel_0_x: 1 from docs")).unwrap_err();
1895 assert!(
1896 e.message.contains("reserved prefix `__omg_`"),
1897 "{}",
1898 e.message
1899 );
1900 assert!(
1902 check_reserved_aliases(&parse(
1903 "select __oqx: 1, x: _nodes collect { select __oqx_y: 1 } from docs"
1904 ))
1905 .is_ok()
1906 );
1907 }
1908
1909 #[test]
1910 fn semantic_phrases_are_collected_distinct() {
1911 let phrases = collect_semantic_phrases(
1912 "select s: semantic(\"alpha\") from docs where semantic(\"alpha\") > 0.5 || _nodes exists { where semantic(\"beta\") > 0 } order by semantic(\"gamma\") desc",
1913 );
1914 assert_eq!(phrases, ["alpha", "beta", "gamma"]);
1915 assert!(collect_semantic_phrases("not a query {{").is_empty());
1916 assert!(collect_semantic_phrases("from docs").is_empty());
1917 }
1918
1919 #[test]
1920 fn hits_peel_the_injected_columns() {
1921 let mut o = oqx::Object::new();
1922 o.insert(ID_KEY, Value::Str("d_1".into()));
1923 o.insert(PATH_KEY, Value::Null);
1924 o.insert("layer", Value::Str("canon".into()));
1925 let shaped = shape_row(Value::Object(o)).unwrap();
1926 assert_eq!((shaped.id.as_str(), shaped.path.as_str()), ("d_1", ""));
1927 let hit = to_hit(shaped);
1928 let ho = hit.as_object().unwrap();
1929 assert_eq!(ho.keys().collect::<Vec<_>>(), ["_id", "_path", "layer"]);
1930 assert_eq!(ho.get("_path"), Some(&Value::Str(String::new())));
1931 let mut o = oqx::Object::new();
1934 o.insert(ID_KEY, Value::Str("d_1".into()));
1935 o.insert(PATH_KEY, Value::Str("a.md".into()));
1936 o.insert("_id", Value::Number(7.0));
1937 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1938 let ho = hit.as_object().unwrap();
1939 assert_eq!(ho.keys().collect::<Vec<_>>(), ["_id", "_path"]);
1940 assert_eq!(ho.get("_id"), Some(&Value::Number(7.0)));
1941 let mut o = oqx::Object::new();
1943 o.insert(ID_KEY, Value::Str("d_1".into()));
1944 o.insert(PATH_KEY, Value::Str("a.md".into()));
1945 o.insert("path", Value::Str("authored".into()));
1946 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1947 assert_eq!(
1948 hit.as_object().unwrap().keys().collect::<Vec<_>>(),
1949 ["_id", "_path", "path"]
1950 );
1951 let mut o = oqx::Object::new();
1953 o.insert(ID_KEY, Value::Str("d_1".into()));
1954 o.insert(PATH_KEY, Value::Undefined);
1955 let row = Value::Object(o);
1956 assert_eq!(raw_identity(&row), (String::new(), "d_1".to_owned()));
1957 let shaped = shape_row(row).unwrap();
1958 assert_eq!((shaped.path, shaped.id), (String::new(), "d_1".to_owned()));
1959 }
1960
1961 #[test]
1962 fn bare_hits_are_the_projection_in_its_own_order() {
1963 let mut o = oqx::Object::new();
1966 o.insert(ID_KEY, Value::Str("d_1".into()));
1967 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1968 o.insert("_title", Value::Str("T".into()));
1969 o.insert("_id", Value::Str("d_1".into()));
1970 let shaped = shape_row(Value::Object(o)).unwrap();
1971 let bare = to_bare_hit(shaped);
1972 let bo = bare.as_object().unwrap();
1973 assert_eq!(bo.keys().collect::<Vec<_>>(), ["_title", "_id"]);
1974 let mut o = oqx::Object::new();
1977 o.insert(ID_KEY, Value::Str("d_1".into()));
1978 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1979 o.insert("_path", Value::Str("canon".into()));
1980 let shaped = shape_row(Value::Object(o)).unwrap();
1981 assert_eq!(shaped.path, "/a.md");
1982 assert_eq!(
1983 render_hit(shaped, true),
1984 serde_json::json!({ "_path": "canon" })
1985 );
1986 let mut o = oqx::Object::new();
1988 o.insert(ID_KEY, Value::Str("d_1".into()));
1989 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1990 o.insert("_path", Value::Str("/a.md".into()));
1991 assert_eq!(
1992 render_hit(shape_row(Value::Object(o)).unwrap(), false),
1993 serde_json::json!({ "_id": "d_1", "_path": "/a.md" })
1994 );
1995 }
1996
1997 #[test]
1998 fn bare_needs_a_select_and_is_not_the_values_form() {
1999 let parse = |s: &str| oqx::parse_string(s).unwrap();
2000 for q in ["from docs", "from docs where layer == \"canon\""] {
2001 let e = check_bare(&parse(q)).unwrap_err();
2002 assert_eq!(e.code, "filter_invalid", "{q}");
2003 assert_eq!(e.message, BARE_NEEDS_SELECT_MESSAGE, "{q}");
2004 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
2005 }
2006 let e = check_bare(&parse("select _path values from docs")).unwrap_err();
2007 assert_eq!(e.code, "filter_invalid");
2008 assert_eq!(e.message, BARE_WITH_VALUES_MESSAGE);
2009 for q in [
2010 "select _path from docs",
2011 "select _id, _title from docs",
2012 "docs first { select t: _title }",
2013 ] {
2014 check_bare(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
2015 }
2016 }
2017
2018 #[test]
2019 fn a_projection_onto_the_envelope_must_be_aliased() {
2020 let items = |src: &str| oqx::parse_string(src).unwrap().select;
2021 assert!(envelope_collision(&items("select _id, _path from docs")).is_none());
2023 assert!(
2024 envelope_collision(&items("select _id: _id, _path: _path, p: _path from docs"))
2025 .is_none()
2026 );
2027 assert!(envelope_collision(&items("select id, path, d: _data.path from docs")).is_none());
2029 let e = envelope_collision(&items("select _path: path from docs")).unwrap();
2031 assert_eq!(e.code, "filter_invalid");
2032 assert_eq!(
2033 e.message,
2034 "projection `_path` collides with the hit envelope — alias it (`p: …`)"
2035 );
2036 let e = envelope_collision(&items("select _id: layer from docs")).unwrap();
2037 assert_eq!(
2038 e.message,
2039 "projection `_id` collides with the hit envelope — alias it (`i: …`)"
2040 );
2041 assert!(envelope_collision(&items("select _path: _doc._path from blocks")).is_some());
2042 assert!(envelope_collision(&items("select _path: _nodes collect { } from docs")).is_some());
2043 }
2044
2045 #[test]
2046 fn a_flatten_bound_onto_the_envelope_is_refused() {
2047 let flatten = |src: &str| oqx::parse_string(src).unwrap().flatten;
2048 assert!(
2050 flatten_collision(&flatten("select _path, t from docs flatten _tags as t")).is_none()
2051 );
2052 assert!(
2053 flatten_collision(&flatten(
2054 "select _path, b from docs flatten _blocks { where _type == \"task\" } as b flatten b._children as c"
2055 ))
2056 .is_none()
2057 );
2058 assert!(flatten_collision(&flatten("select _path from docs")).is_none());
2059 assert!(
2061 flatten_collision(&flatten("select _path from docs flatten _tags as id")).is_none()
2062 );
2063 let e = flatten_collision(&flatten("select _path from docs flatten _tags as _id")).unwrap();
2065 assert_eq!(e.code, "filter_invalid");
2066 assert_eq!(
2067 e.message,
2068 "flatten `as _id` collides with the hit envelope — a flattened row is still the row, whose `_id` the envelope reads; bind another name"
2069 );
2070 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
2071 let e = flatten_collision(&flatten(
2072 "select _title from docs flatten _tags as t flatten paths as _path",
2073 ))
2074 .unwrap();
2075 assert_eq!(
2076 e.message,
2077 "flatten `as _path` collides with the hit envelope — a flattened row is still the row, whose `_path` the envelope reads; bind another name"
2078 );
2079 }
2080
2081 #[test]
2082 fn a_path_read_is_the_attribute_or_a_row_handle_never_a_source_handle() {
2083 let pred = |src: &str| match oqx::parse_string(src).unwrap().r#where.unwrap() {
2084 oqx::ast::Where::Scalar { expr, .. } => expr,
2085 other => panic!("{other:?}"),
2086 };
2087 let left = |e: Expr| match e {
2088 Expr::Binary { left, .. } => *left,
2089 other => panic!("{other:?}"),
2090 };
2091 assert!(is_path_read(&left(pred("from docs where _path == \"x\""))));
2092 assert!(is_path_read(&left(pred(
2093 "from blocks where _doc._path == \"x\""
2094 ))));
2095 assert!(is_path_read(&left(pred("from docs where ^_path == \"x\""))));
2096 assert!(is_path_read(&left(pred(
2097 "from links where _dst_path == \"x\""
2098 ))));
2099 assert!(is_path_read(&left(pred(
2100 "from links where _block._path == \"x\""
2101 ))));
2102 assert!(!is_path_read(&left(pred("from docs where path == \"x\""))));
2104 assert!(!is_path_read(&left(pred(
2105 "from docs where _data.path == \"x\""
2106 ))));
2107 assert!(!is_path_read(&left(pred(
2108 "from docs where _frontmatter.path == \"x\""
2109 ))));
2110 }
2111
2112 #[test]
2113 fn a_hit_that_is_not_a_store_row_fails_the_query() {
2114 let mut o = oqx::Object::new();
2117 o.insert(ID_KEY, Value::Undefined);
2118 o.insert(PATH_KEY, Value::Undefined);
2119 o.insert(SELF_KEY, Value::Str("/timeline/kickoff.md".into()));
2120 let e = shape_row(Value::Object(o)).unwrap_err();
2121 assert_eq!(e.code, "filter_invalid");
2122 assert_eq!(
2123 e.message,
2124 "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>)"
2125 );
2126 let mut row = oqx::Object::new();
2128 row.insert("doc_id", Value::Str("d_1".into()));
2129 row.insert("path", Value::Str("a.md".into()));
2130 let row = crate::context::tag_row(row, crate::context::Target::Docs);
2131 let mut o = oqx::Object::new();
2132 o.insert(ID_KEY, Value::Str("d_1".into()));
2133 o.insert(PATH_KEY, Value::Str("a.md".into()));
2134 o.insert(SELF_KEY, row);
2135 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
2136 assert_eq!(
2137 hit.as_object().unwrap().keys().collect::<Vec<_>>(),
2138 ["_id", "_path"]
2139 );
2140 let parse = |s: &str| oqx::parse_string(s).unwrap();
2142 assert!(!may_reach_non_rows(&parse(
2143 "from docs where layer == \"canon\""
2144 )));
2145 assert!(may_reach_non_rows(&parse("from docs follow before")));
2146 assert!(may_reach_non_rows(&parse("refs(\"/index.md\") first { }")));
2147 }
2148
2149 #[test]
2150 fn distinct_dedups_by_user_projection_first_wins() {
2151 let mk = |id: &str, t: &str| {
2152 let mut proj = oqx::Object::new();
2153 proj.insert("type", Value::Str(t.into()));
2154 Shaped {
2155 id: id.to_owned(),
2156 path: "p".to_owned(),
2157 proj,
2158 }
2159 };
2160 let out = dedup_by_projection(vec![mk("1", "a"), mk("2", "b"), mk("3", "a")], false);
2161 assert_eq!(out.len(), 2);
2162 assert_eq!(out[0].id, "1");
2163 assert_eq!(out[1].id, "2");
2164 let with_id = |id: &str| {
2168 let mut proj = oqx::Object::new();
2169 proj.insert("_id", Value::Str(id.into()));
2170 Shaped {
2171 id: id.to_owned(),
2172 path: "p".to_owned(),
2173 proj,
2174 }
2175 };
2176 assert_eq!(
2177 dedup_by_projection(vec![with_id("1"), with_id("2")], false).len(),
2178 1
2179 );
2180 assert_eq!(
2181 dedup_by_projection(vec![with_id("1"), with_id("2")], true).len(),
2182 2
2183 );
2184 }
2185
2186 #[test]
2187 fn the_page_runs_on_identity() {
2188 let rows: Vec<(String, String)> = [("/a.md", "d_1"), ("/a.md", "d_2"), ("/b.md", "d_3")]
2189 .iter()
2190 .map(|(p, i)| ((*p).to_owned(), (*i).to_owned()))
2191 .collect();
2192 let ident = |r: &(String, String)| (r.0.clone(), r.1.clone());
2193 let (page, truncated, next) =
2195 page_rows(rows.clone(), ident, 0, None, None, 2, false).unwrap();
2196 assert_eq!(page.len(), 2);
2197 assert!(truncated);
2198 let next = next.expect("a cursor");
2199 assert_eq!(next, encode_cursor(&["/a.md", "d_2"]));
2200 let (page, truncated, next) =
2202 page_rows(rows.clone(), ident, 0, None, Some(&next), 2, false).unwrap();
2203 assert_eq!(page, vec![("/b.md".to_owned(), "d_3".to_owned())]);
2204 assert!(!truncated && next.is_none());
2205 let (page, truncated, next) =
2207 page_rows(rows.clone(), ident, 1, Some(1), None, 50, true).unwrap();
2208 assert_eq!(page, vec![("/a.md".to_owned(), "d_2".to_owned())]);
2209 assert!(!truncated && next.is_none());
2210 let (page, truncated, next) = page_rows(rows, ident, 0, None, None, 1, true).unwrap();
2211 assert_eq!(page.len(), 1);
2212 assert!(truncated && next.is_none());
2213 }
2214
2215 #[test]
2216 fn top_level_bounds_must_be_literals() {
2217 assert_eq!(const_bound(None, "limit").unwrap(), None);
2218 assert_eq!(
2219 const_bound(
2220 Some(&Expr::Lit {
2221 value: Value::Number(3.0),
2222 span: oqx::Span::EMPTY
2223 }),
2224 "limit"
2225 )
2226 .unwrap(),
2227 Some(3)
2228 );
2229 let e = const_bound(
2230 Some(&Expr::Lit {
2231 value: Value::Number(-1.0),
2232 span: oqx::Span::EMPTY,
2233 }),
2234 "offset",
2235 )
2236 .unwrap_err();
2237 assert_eq!(e.code, "filter_invalid");
2238 assert!(e.message.contains("top-level offset"));
2239 }
2240}