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 && Target::parse(name).is_none()
487 {
488 return Err(SurfaceError::filter_invalid(
489 unknown_target_message(name),
490 "OQX",
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, .. }) if ctx.depth >= 1 => {
509 if let Some(t) = Target::parse(name) {
510 self.0 = Some(SurfaceError::filter_invalid(
511 bare_target_message(t, Some(ctx.depth)),
512 "OQX",
513 ));
514 }
515 }
516 Node::Expr(Expr::Outer { name, levels, .. }) if *levels > ctx.depth => {
517 if let Some(t) = Target::parse(name) {
518 self.0 = Some(SurfaceError::filter_invalid(
519 past_root_message(t, *levels),
520 "OQX",
521 ));
522 }
523 }
524 _ => {}
525 }
526 self.0.is_none()
527 }
528 }
529 let mut check = Check(None);
530 visit(Node::Query(q), &mut check);
531 check.0.map_or(Ok(()), Err)
532}
533
534struct Phrases(Vec<String>);
538
539impl Visitor<'_> for Phrases {
540 fn enter(&mut self, node: Node<'_>, _ctx: &VisitContext<'_>) -> bool {
541 if let Node::Expr(Expr::Call {
542 recv: None,
543 name,
544 args,
545 ..
546 }) = node
547 && name == "semantic"
548 && let Some(Expr::Lit {
549 value: Value::Str(s),
550 ..
551 }) = args.first()
552 && !self.0.contains(s)
553 {
554 self.0.push(s.clone());
555 }
556 true
557 }
558}
559
560#[must_use]
563pub fn collect_semantic_phrases(source: &str) -> Vec<String> {
564 let Ok(q) = oqx::parse_string(source) else {
565 return Vec::new();
566 };
567 semantic_phrases_of(&q)
568}
569
570#[must_use]
572pub fn semantic_phrases_of(q: &Query) -> Vec<String> {
573 let mut phrases = Phrases(Vec::new());
574 visit(Node::Query(q), &mut phrases);
575 phrases.0
576}
577
578pub fn embed_phrases(
585 provider: Option<&dyn EmbeddingProvider>,
586 phrases: Vec<String>,
587) -> Result<HashMap<String, SemanticVec>> {
588 let mut semantic: HashMap<String, SemanticVec> = HashMap::new();
589 if let Some(provider) = provider.filter(|_| !phrases.is_empty()) {
590 for phrase in phrases {
591 let vec = provider
592 .embed_query(&phrase)
593 .map_err(|e| SurfaceError::new(e.code(), e.to_string()))?;
594 semantic.insert(
595 phrase,
596 SemanticVec {
597 model: provider.model().to_owned(),
598 vec: f32_to_blob(&vec),
599 },
600 );
601 }
602 }
603 Ok(semantic)
604}
605
606pub const RESERVED_ALIAS_PREFIXES: [&str; 2] = ["__oqx_", "__omg_"];
618
619pub fn check_reserved_aliases(q: &Query) -> Result<()> {
621 for item in &q.select {
622 if let Some(prefix) = RESERVED_ALIAS_PREFIXES
623 .iter()
624 .find(|p| item.name().starts_with(*p))
625 {
626 return Err(SurfaceError::filter_invalid(
627 format!(
628 "projection `{}` uses the reserved prefix `{prefix}` — choose another alias",
629 item.name()
630 ),
631 "OQX",
632 ));
633 }
634 }
635 Ok(())
636}
637
638fn mentions_outside_source(q: &Query, name: &str) -> bool {
644 struct Mentions<'n> {
645 name: &'n str,
646 found: bool,
647 }
648 impl Visitor<'_> for Mentions<'_> {
649 fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
650 if self.found {
651 return false;
652 }
653 if ctx.clause == Some(Clause::Source) && ctx.path.len() == 1 {
655 return false;
656 }
657 if let Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. }) = node
658 && name == self.name
659 {
660 self.found = true;
661 }
662 !self.found
663 }
664 }
665 let mut m = Mentions { name, found: false };
666 visit(Node::Query(q), &mut m);
667 m.found
668}
669
670fn may_reach_non_rows(q: &Query) -> bool {
678 q.follow.is_some() || !q.from.is_empty() || root_target(&q.source).is_none()
679}
680
681fn describe_value(v: &Value) -> String {
683 match v {
684 Value::Undefined | Value::Null => "an absent value".to_owned(),
685 Value::Str(s) => format!(
686 "a string ({})",
687 serde_json::to_string(s).unwrap_or_default()
688 ),
689 Value::Number(_) => format!("a number ({v})"),
690 Value::Bool(b) => format!("a boolean ({b})"),
691 Value::Array(_) => "an array".to_owned(),
692 Value::Range(_) => "a range".to_owned(),
693 Value::Object(_) => "an object".to_owned(),
694 }
695}
696
697fn not_a_store_row(v: &Value) -> SurfaceError {
698 SurfaceError::filter_invalid(
699 format!(
700 "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>)",
701 describe_value(v)
702 ),
703 "OQX",
704 )
705}
706
707#[derive(Debug)]
717struct Shaped {
718 id: String,
719 path: String,
720 proj: oqx::Object,
721}
722
723fn path_string(path: &Value) -> String {
724 if path.is_absent() {
725 String::new()
726 } else {
727 path.to_string()
728 }
729}
730
731fn shape_row(row: Value) -> Result<Shaped> {
732 let mut shaped = Shaped {
733 id: Value::Undefined.to_string(),
734 path: String::new(),
735 proj: oqx::Object::new(),
736 };
737 let Value::Object(mut o) = row else {
738 return Ok(shaped);
739 };
740 if let Some(me) = o.remove(SELF_KEY)
741 && target_of(&me).is_none()
742 {
743 return Err(not_a_store_row(&me));
744 }
745 let Value::Object(o) = render_row_values(Value::Object(o)) else {
746 return Ok(shaped);
747 };
748 for (k, v) in o {
749 match k.as_str() {
750 ID_KEY => shaped.id = v.to_string(),
751 PATH_KEY => shaped.path = path_string(&v),
752 _ => {
753 shaped.proj.insert(k, v);
754 }
755 }
756 }
757 Ok(shaped)
758}
759
760fn raw_identity(row: &Value) -> (String, String) {
765 let get = |k: &str| {
766 row.as_object()
767 .and_then(|o| o.get(k))
768 .cloned()
769 .unwrap_or(Value::Undefined)
770 };
771 (path_string(&get(PATH_KEY)), get(ID_KEY).to_string())
772}
773
774fn to_hit(s: Shaped) -> Value {
779 let mut hit = oqx::Object::with_capacity(s.proj.len() + 2);
780 hit.insert(HIT_ID, Value::Str(s.id));
781 hit.insert(HIT_PATH, Value::Str(s.path));
782 for (k, v) in s.proj {
783 hit.insert(k, v);
784 }
785 Value::Object(hit)
786}
787
788fn to_bare_hit(s: Shaped) -> Value {
793 Value::Object(s.proj)
794}
795
796fn render_hit(s: Shaped, bare: bool) -> Json {
798 if bare {
799 to_bare_hit(s).to_canonical_json()
800 } else {
801 to_hit(s).to_canonical_json()
802 }
803}
804
805fn dedup_by_projection(rows: Vec<Shaped>, bare: bool) -> Vec<Shaped> {
814 let mut seen: Vec<String> = Vec::new();
815 let mut out = Vec::new();
816 for s in rows {
817 let mut pairs: Vec<(String, Value)> = s
818 .proj
819 .iter()
820 .filter(|(k, _)| bare || (*k != HIT_ID && *k != HIT_PATH))
821 .map(|(k, v)| (k.to_owned(), v.clone()))
822 .collect();
823 pairs.sort_by(|a, b| a.0.cmp(&b.0));
824 let key = Value::Array(
825 pairs
826 .into_iter()
827 .map(|(k, v)| Value::Array(vec![Value::Str(k), v]))
828 .collect(),
829 )
830 .to_canonical_json()
831 .to_string();
832 if seen.contains(&key) {
833 continue;
834 }
835 seen.push(key);
836 out.push(s);
837 }
838 out
839}
840
841fn const_bound(e: Option<&Expr>, word: &str) -> Result<Option<usize>> {
844 match e {
845 None => Ok(None),
846 Some(Expr::Lit {
847 value: Value::Number(n),
848 ..
849 }) if n.fract() == 0.0 && *n >= 0.0 && n.is_finite() => Ok(Some(*n as usize)),
850 Some(_) => Err(SurfaceError::filter_invalid(
851 format!("top-level {word} must be a non-negative integer literal"),
852 "OQX",
853 )),
854 }
855}
856
857fn page_rows<T>(
863 mut rows: Vec<T>,
864 ident: impl Fn(&T) -> (String, String),
865 offset: usize,
866 limit: Option<usize>,
867 cursor: Option<&str>,
868 cap: usize,
869 custom: bool,
870) -> Result<(Vec<T>, bool, Option<String>)> {
871 if offset > 0 || limit.is_some() {
872 let end = limit.map_or(rows.len(), |l| (offset + l).min(rows.len()));
873 if offset >= rows.len() {
874 rows.clear();
875 } else {
876 rows.truncate(end);
877 rows.drain(..offset);
878 }
879 }
880 if let Some(cursor) = cursor {
881 let parts = decode_path_cursor(cursor, "query", 2)?;
883 let (path, id) = (&parts[0], &parts[1]);
884 rows.retain(|r| {
885 let (rp, ri) = ident(r);
886 rp > *path || (rp == *path && ri > *id)
887 });
888 }
889 let truncated = rows.len() > cap;
890 rows.truncate(cap);
891 let next = if truncated && !custom {
892 rows.last().map(|last| {
893 let (p, i) = ident(last);
894 encode_cursor(&[&p, &i])
895 })
896 } else {
897 None
898 };
899 Ok((rows, truncated, next))
900}
901
902fn value_of(s: &Shaped) -> Json {
903 s.proj
904 .get(VALUE_KEY)
905 .cloned()
906 .unwrap_or(Value::Undefined)
907 .to_canonical_json()
908}
909
910pub fn check_bare(q: &Query) -> Result<()> {
917 if q.values {
918 return Err(SurfaceError::filter_invalid(
919 BARE_WITH_VALUES_MESSAGE,
920 "OQX",
921 ));
922 }
923 if q.select.is_empty() {
924 return Err(SurfaceError::filter_invalid(
925 BARE_NEEDS_SELECT_MESSAGE,
926 "OQX",
927 ));
928 }
929 Ok(())
930}
931
932pub const BARE_NEEDS_SELECT_MESSAGE: &str =
934 "`bare` needs a `select` — without a projection a hit is only its envelope";
935pub const BARE_WITH_VALUES_MESSAGE: &str =
936 "`bare` is redundant with `values` — `values` is already the bare-values form; drop one";
937
938pub fn query(
942 store: &Store,
943 repo_id: &str,
944 source: &str,
945 opts: QueryOptions<'_>,
946) -> Result<OqxResult> {
947 let semantic = embed_phrases(opts.provider, collect_semantic_phrases(source))?;
948 let runner = Runner {
949 store,
950 repo_id,
951 semantic,
952 planned: !opts.in_memory,
953 now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
954 };
955 let raw = oqx::parse_string(source).map_err(|e| parse_invalid(source, &e, opts.request))?;
956 check_reserved_aliases(&raw)?;
957 run_parsed(
959 &runner,
960 &raw,
961 opts,
962 opts.unknown_names.unwrap_or(UnknownNames::Hint),
963 )
964}
965
966const BRACKET_ERROR_HEAD: &str = "unexpected '[' — expected a value (at offset ";
979pub const TUPLE_HINT: &str = "; a list is written as a tuple: (…)";
981
982fn bracket_error_offset(message: &str) -> Option<usize> {
985 let digits = message
986 .strip_prefix(BRACKET_ERROR_HEAD)?
987 .strip_suffix(')')?;
988 if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
989 return None;
990 }
991 digits.parse().ok()
992}
993
994#[must_use]
998pub fn bracket_list_as_tuple(source: &str, message: &str) -> Option<String> {
999 let open = bracket_error_offset(message)?;
1000 let mut cps: Vec<char> = source.chars().collect();
1001 if cps.get(open) != Some(&'[') {
1002 return None;
1003 }
1004 let mut i = open + 1;
1005 let mut members = 0;
1006 let close = loop {
1007 i = skip_spaces(&cps, i);
1008 let end = literal_end(&cps, i)?;
1009 members += 1;
1010 i = skip_spaces(&cps, end);
1011 match cps.get(i) {
1012 Some(',') => i += 1,
1013 Some(']') => break i,
1014 _ => return None,
1015 }
1016 };
1017 if members < 2 {
1018 return None;
1019 }
1020 cps[open] = '(';
1021 cps[close] = ')';
1022 Some(cps.into_iter().collect())
1023}
1024
1025fn skip_spaces(cps: &[char], mut i: usize) -> usize {
1026 while i < cps.len() && cps[i].is_whitespace() {
1027 i += 1;
1028 }
1029 i
1030}
1031
1032fn literal_end(cps: &[char], i: usize) -> Option<usize> {
1036 let c = *cps.get(i)?;
1037 if c == '"' || c == '\'' {
1038 let mut j = i + 1;
1039 while j < cps.len() {
1040 if cps[j] == '\\' {
1041 j += 2;
1042 } else if cps[j] == c {
1043 return Some(j + 1);
1044 } else {
1045 j += 1;
1046 }
1047 }
1048 return None;
1049 }
1050 if c.is_ascii_digit() {
1051 let digits = |mut j: usize| {
1052 while j < cps.len() && cps[j].is_ascii_digit() {
1053 j += 1;
1054 }
1055 j
1056 };
1057 let mut j = digits(i);
1058 if cps.get(j) == Some(&'.') && cps.get(j + 1).is_some_and(char::is_ascii_digit) {
1059 j = digits(j + 1);
1060 }
1061 if matches!(cps.get(j), Some('e' | 'E')) {
1062 let mut k = j + 1;
1063 if matches!(cps.get(k), Some('+' | '-')) {
1064 k += 1;
1065 }
1066 if cps.get(k).is_some_and(char::is_ascii_digit) {
1067 j = digits(k);
1068 }
1069 }
1070 return Some(j);
1071 }
1072 for word in ["true", "false", "null"] {
1073 let n = word.len();
1074 let matches_word = cps.len() >= i + n && cps[i..i + n].iter().copied().eq(word.chars());
1075 let boundary = !cps
1076 .get(i + n)
1077 .is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '$');
1078 if matches_word && boundary {
1079 return Some(i + n);
1080 }
1081 }
1082 None
1083}
1084
1085fn parse_invalid(source: &str, e: &oqx::OqxError, request: Option<&Json>) -> SurfaceError {
1089 let Some(rewritten) = bracket_list_as_tuple(source, &e.message) else {
1090 return SurfaceError::filter_invalid(e.message.clone(), "OQX");
1091 };
1092 let mut err = SurfaceError::filter_invalid(format!("{}{TUPLE_HINT}", e.message), "OQX");
1093 if let Some(Json::Object(args)) = request
1094 && let Some(d) = err.data.as_mut().and_then(Json::as_object_mut)
1095 {
1096 let mut fixed = args.clone();
1097 fixed.insert("query".to_owned(), Json::String(rewritten));
1098 d.insert(
1099 "fix".to_owned(),
1100 Json::String(omgbase_mutate::canonical_json(&Json::Object(fixed))),
1101 );
1102 }
1103 err
1104}
1105
1106pub fn query_ast(
1115 store: &Store,
1116 repo_id: &str,
1117 raw: &Query,
1118 opts: QueryOptions<'_>,
1119 semantic: HashMap<String, SemanticVec>,
1120) -> Result<OqxResult> {
1121 let runner = Runner {
1122 store,
1123 repo_id,
1124 semantic,
1125 planned: !opts.in_memory,
1126 now_ms: opts.now_ms.unwrap_or_else(dates::wall_clock_ms),
1127 };
1128 run_parsed(
1129 &runner,
1130 raw,
1131 opts,
1132 opts.unknown_names.unwrap_or(UnknownNames::Refuse),
1133 )
1134}
1135
1136struct Runner<'a> {
1142 store: &'a Store,
1143 repo_id: &'a str,
1144 semantic: HashMap<String, SemanticVec>,
1145 planned: bool,
1146 now_ms: i64,
1148}
1149
1150impl Runner<'_> {
1151 fn run(&self, q: &Query) -> Result<oqx::OqxResult> {
1158 let conn = self.store.conn();
1159 let ctx =
1160 StoreContext::new(conn, self.repo_id, self.semantic.clone()).with_now_ms(self.now_ms);
1161 let plan = if self.planned {
1162 SqlitePlanner::new(conn, self.repo_id)
1163 .try_plan(q, &[])
1164 .map_err(|e| SurfaceError::other(format!("sqlite: {e}")))?
1165 } else {
1166 None
1167 };
1168 let (ctx, residual) = match plan {
1169 Some(plan) => {
1174 let once = !mentions_outside_source(&plan.residual, oqx::ROWS_ROOT);
1175 let ctx = if once {
1176 ctx.with_rows_root_once(plan.rows)
1177 } else {
1178 ctx.with_rows_root(plan.rows)
1179 };
1180 (ctx, Some(plan.residual))
1181 }
1182 None => (ctx, None),
1183 };
1184 let engine = InMemoryEngine::new(ctx);
1185 let out = engine.run(residual.as_ref().unwrap_or(q), &[]);
1186 if let Some(failed) = engine.context().take_root_failure() {
1189 return Err(failed.into());
1190 }
1191 Ok(out?)
1194 }
1195}
1196
1197fn run_parsed(
1198 engine: &Runner<'_>,
1199 raw: &Query,
1200 opts: QueryOptions<'_>,
1201 mode: UnknownNames,
1202) -> Result<OqxResult> {
1203 check_root_spellings(raw)?;
1204 let own = Query {
1212 select: raw
1213 .select
1214 .iter()
1215 .filter(|it| {
1216 !RESERVED_ALIAS_PREFIXES
1217 .iter()
1218 .any(|p| it.name().starts_with(p))
1219 })
1220 .cloned()
1221 .collect(),
1222 ..raw.clone()
1223 };
1224 let local = VocabularyCache::default();
1225 let cache = opts.vocabulary.unwrap_or(&local);
1226 let hints = check_names(engine.store, engine.repo_id, &own, mode, cache)?;
1227 let mut result = run_resolved(engine, raw, opts)?;
1228 result.hints = hints;
1230 Ok(result)
1231}
1232
1233fn run_resolved(engine: &Runner<'_>, raw: &Query, opts: QueryOptions<'_>) -> Result<OqxResult> {
1234 let parsed = rewrite_query(&resolve_aliases(raw)?, engine.now_ms);
1237 let consumer = parsed.consumer;
1238
1239 match consumer {
1240 Consumer::Exists => {
1241 let res = engine.run(&parsed)?;
1242 let mut r = OqxResult::scalar(consumer);
1243 r.exists = Some(matches!(res, oqx::OqxResult::Exists(true)));
1244 return Ok(r);
1245 }
1246 Consumer::Count => {
1247 let res = engine.run(&parsed)?;
1248 let mut r = OqxResult::scalar(consumer);
1249 r.count = Some(match res {
1250 oqx::OqxResult::Count(n) => n,
1251 _ => 0.0,
1252 });
1253 return Ok(r);
1254 }
1255 Consumer::None => {
1256 let res = engine.run(&parsed)?;
1257 let mut r = OqxResult::scalar(consumer);
1258 r.none = Some(match res {
1259 oqx::OqxResult::None(b) => b,
1260 _ => true,
1261 });
1262 return Ok(r);
1263 }
1264 Consumer::Collect | Consumer::First | Consumer::Single => {}
1265 }
1266
1267 if parsed.group_by.is_some() {
1279 return run_grouped(engine, &parsed, consumer, opts);
1280 }
1281
1282 let flattened = !parsed.flatten.is_empty();
1293 if let Some(e) = flatten_collision(&parsed.flatten) {
1294 return Err(e);
1295 }
1296
1297 let top_distinct = parsed.distinct;
1303 let top_values = parsed.values;
1304 let user_select: Vec<SelectItem> = if top_values {
1305 parsed
1306 .select
1307 .first()
1308 .map(|it| match it {
1309 SelectItem::Field {
1310 expr, lift, span, ..
1311 } => SelectItem::Field {
1312 name: VALUE_KEY.to_owned(),
1313 expr: expr.clone(),
1314 lift: *lift,
1315 span: *span,
1316 },
1317 SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1318 name: VALUE_KEY.to_owned(),
1319 op: op.clone(),
1320 span: *span,
1321 },
1322 })
1323 .into_iter()
1324 .collect()
1325 } else {
1326 parsed.select.clone()
1327 };
1328 let bare = opts.bare;
1332 if bare {
1333 check_bare(&parsed)?;
1334 } else if !top_values && let Some(e) = envelope_collision(&user_select) {
1335 return Err(e);
1336 }
1337 let id_item = build::field(ID_KEY, build::ident(HIT_ID));
1338 let path_item = build::field(PATH_KEY, build::ident(HIT_PATH));
1339 let mut select = vec![id_item, path_item];
1340 if !top_values && may_reach_non_rows(&parsed) {
1341 select.push(build::field(SELF_KEY, build::ident("$it")));
1342 }
1343 select.extend(user_select);
1344 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1348 let q = Query {
1349 distinct: false,
1350 values: false,
1351 select,
1352 limit: if consumer == Consumer::Collect {
1353 None
1354 } else {
1355 parsed.limit.clone()
1356 },
1357 offset: if consumer == Consumer::Collect {
1358 None
1359 } else {
1360 parsed.offset.clone()
1361 },
1362 ..parsed.clone()
1363 };
1364 let res = engine.run(&q)?;
1365
1366 if matches!(consumer, Consumer::First | Consumer::Single) {
1367 let row = match res {
1368 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1369 _ => None,
1370 };
1371 let mut out = OqxResult::scalar(consumer);
1372 match row {
1373 None => {
1374 if top_values {
1375 out.values = Some(Vec::new());
1376 }
1377 }
1378 Some(r) => {
1379 let shaped = shape_row(r)?;
1380 if top_values {
1381 out.values = Some(vec![value_of(&shaped)]);
1382 } else {
1383 out.hits = vec![render_hit(shaped, bare)];
1384 }
1385 }
1386 }
1387 return Ok(out);
1388 }
1389
1390 let rows: Vec<Value> = match res {
1392 oqx::OqxResult::Collect(rows) => rows,
1393 _ => Vec::new(),
1394 };
1395 let custom = parsed.order_by.as_ref().is_some_and(|o| !o.is_empty()) || flattened;
1396 let cursor = opts.cursor.filter(|c| !c.is_empty() && !custom);
1397 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1398 let limit = const_bound(top_limit.as_ref(), "limit")?;
1399 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1400 let (page, truncated, next) = if top_distinct {
1406 let shaped = rows
1407 .into_iter()
1408 .map(shape_row)
1409 .collect::<Result<Vec<_>>>()?;
1410 let shaped = dedup_by_projection(shaped, bare);
1411 page_rows(
1412 shaped,
1413 |s| (s.path.clone(), s.id.clone()),
1414 offset,
1415 limit,
1416 cursor,
1417 cap,
1418 custom,
1419 )?
1420 } else {
1421 let (page, truncated, next) =
1422 page_rows(rows, raw_identity, offset, limit, cursor, cap, custom)?;
1423 let page = page
1424 .into_iter()
1425 .map(shape_row)
1426 .collect::<Result<Vec<_>>>()?;
1427 (page, truncated, next)
1428 };
1429 let mut out = OqxResult::scalar(Consumer::Collect);
1430 out.truncated = truncated;
1431 out.cursor = next;
1432 if top_values {
1433 out.values = Some(page.iter().map(value_of).collect());
1434 } else {
1435 out.hits = page.into_iter().map(|s| render_hit(s, bare)).collect();
1436 }
1437 Ok(out)
1438}
1439
1440fn run_grouped(
1442 engine: &Runner<'_>,
1443 parsed: &Query,
1444 consumer: Consumer,
1445 opts: QueryOptions<'_>,
1446) -> Result<OqxResult> {
1447 let top_values = parsed.values;
1448 let select: Vec<SelectItem> = if top_values {
1451 parsed
1452 .select
1453 .first()
1454 .map(|it| match it {
1455 SelectItem::Field {
1456 expr, lift, span, ..
1457 } => SelectItem::Field {
1458 name: VALUE_KEY.to_owned(),
1459 expr: expr.clone(),
1460 lift: *lift,
1461 span: *span,
1462 },
1463 SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1464 name: VALUE_KEY.to_owned(),
1465 op: op.clone(),
1466 span: *span,
1467 },
1468 })
1469 .into_iter()
1470 .collect()
1471 } else {
1472 parsed.select.clone()
1473 };
1474 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1475 let q = Query {
1476 values: false,
1477 select,
1478 limit: if consumer == Consumer::Collect {
1479 None
1480 } else {
1481 parsed.limit.clone()
1482 },
1483 offset: if consumer == Consumer::Collect {
1484 None
1485 } else {
1486 parsed.offset.clone()
1487 },
1488 ..parsed.clone()
1489 };
1490 let res = engine.run(&q)?;
1491 let project = |row: Value| -> oqx::Object {
1492 match render_row_values(row) {
1493 Value::Object(o) => o,
1494 _ => oqx::Object::new(),
1495 }
1496 };
1497 let value_of = |g: &oqx::Object| -> Json {
1498 g.get(VALUE_KEY)
1499 .cloned()
1500 .unwrap_or(Value::Undefined)
1501 .to_canonical_json()
1502 };
1503 if matches!(consumer, Consumer::First | Consumer::Single) {
1504 let row = match res {
1505 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1506 _ => None,
1507 };
1508 let mut out = OqxResult::scalar(consumer);
1509 match row {
1510 None => {
1511 if top_values {
1512 out.values = Some(Vec::new());
1513 }
1514 }
1515 Some(r) => {
1516 let g = project(r);
1517 if top_values {
1518 out.values = Some(vec![value_of(&g)]);
1519 } else {
1520 out.hits = vec![Value::Object(g).to_canonical_json()];
1521 }
1522 }
1523 }
1524 return Ok(out);
1525 }
1526 let mut groups: Vec<oqx::Object> = match res {
1527 oqx::OqxResult::Collect(rows) => rows.into_iter().map(project).collect(),
1528 _ => Vec::new(),
1529 };
1530 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1531 let limit = const_bound(top_limit.as_ref(), "limit")?;
1532 if offset > 0 || limit.is_some() {
1533 let end = limit.map_or(groups.len(), |l| (offset + l).min(groups.len()));
1534 if offset >= groups.len() {
1535 groups.clear();
1536 } else {
1537 groups.truncate(end);
1538 groups.drain(..offset);
1539 }
1540 }
1541 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1542 let truncated = groups.len() > cap;
1543 groups.truncate(cap);
1544 let mut out = OqxResult::scalar(Consumer::Collect);
1545 out.truncated = truncated;
1546 if top_values {
1547 out.values = Some(groups.iter().map(value_of).collect());
1548 } else {
1549 out.hits = groups
1550 .into_iter()
1551 .map(|g| Value::Object(g).to_canonical_json())
1552 .collect();
1553 }
1554 Ok(out)
1555}
1556
1557#[cfg(test)]
1558mod tests {
1559 use super::*;
1560 use oqx::ast::Where;
1561 use serde_json::json;
1562
1563 fn parse_err(src: &str) -> String {
1566 oqx::parse_string(src).unwrap_err().message
1567 }
1568
1569 #[test]
1570 fn a_bracket_list_of_literals_is_rewritten_to_a_tuple() {
1571 let cases = [
1572 (
1573 "from docs where stage in [\"nigredo\", \"albedo\"]",
1574 "from docs where stage in (\"nigredo\", \"albedo\")",
1575 ),
1576 (
1577 "from blocks where _depth in [0, 1] and _path == \"/index.md\"",
1578 "from blocks where _depth in (0, 1) and _path == \"/index.md\"",
1579 ),
1580 (
1581 "from docs where verified in [ true, 'yes', null ]",
1582 "from docs where verified in ( true, 'yes', null )",
1583 ),
1584 (
1585 "from docs where x in [1.5e3,2E-1,\"a\\\"b\"]",
1586 "from docs where x in (1.5e3,2E-1,\"a\\\"b\")",
1587 ),
1588 (
1590 "from docs where _title in [\"é\", \"ü\"] and stage in [\"a\", \"b\"]",
1591 "from docs where _title in (\"é\", \"ü\") and stage in [\"a\", \"b\"]",
1592 ),
1593 ];
1594 for (src, want) in cases {
1595 let msg = parse_err(src);
1596 assert!(msg.starts_with(BRACKET_ERROR_HEAD), "{src}: {msg}");
1597 assert_eq!(
1598 bracket_list_as_tuple(src, &msg).as_deref(),
1599 Some(want),
1600 "{src}"
1601 );
1602 }
1603 }
1604
1605 #[test]
1606 fn a_bracket_list_that_is_not_two_or_more_literals_has_no_rewrite() {
1607 for src in [
1608 "from docs where stage in [[\"nigredo\"], \"albedo\"]",
1609 "from docs where stage in [stage, \"albedo\"]",
1610 "from docs where stage in [\"nigredo\"]",
1611 "from docs where stage in [\"nigredo\", \"albedo\"",
1612 "from docs where stage in [\"nigredo\" \"albedo\"]",
1613 "from docs where stage in [truex, 1]",
1614 "from docs where stage in [-1, 2]",
1615 "from docs where stage in []",
1616 ] {
1617 let msg = parse_err(src);
1618 assert_eq!(bracket_list_as_tuple(src, &msg), None, "{src}: {msg}");
1619 }
1620 assert_eq!(bracket_list_as_tuple("from docs", "unexpected end"), None);
1622 assert_eq!(
1623 bracket_list_as_tuple(
1624 "from docs",
1625 "unexpected '[' — expected a value (at offset 2)"
1626 ),
1627 None
1628 );
1629 assert_eq!(
1630 bracket_error_offset("unexpected '[' — expected a value (at offset x)"),
1631 None
1632 );
1633 assert_eq!(
1634 bracket_error_offset("unexpected '[' — expected a value (at offset 7)"),
1635 Some(7)
1636 );
1637 }
1638
1639 #[test]
1640 fn a_parse_error_carries_the_hint_and_the_corrected_call() {
1641 let src = "select _path from docs where stage in [\"nigredo\", \"albedo\"]";
1642 let e = oqx::parse_string(src).unwrap_err();
1643 let request = json!({ "query": src, "limit": 5, "bare": true });
1644 let err = parse_invalid(src, &e, Some(&request));
1645 assert_eq!(err.code, "filter_invalid");
1646 assert_eq!(
1647 err.message,
1648 "unexpected '[' — expected a value (at offset 38); a list is written as a tuple: (…)"
1649 );
1650 let d = err.data.unwrap();
1651 assert_eq!(d["hint"], "OQX");
1652 assert_eq!(d["reason"], err.message);
1653 assert_eq!(
1654 d["fix"],
1655 r#"{"bare":true,"limit":5,"query":"select _path from docs where stage in (\"nigredo\", \"albedo\")"}"#
1656 );
1657 let err = parse_invalid(src, &e, None);
1659 assert!(err.message.ends_with(TUPLE_HINT));
1660 assert!(err.data.unwrap().get("fix").is_none());
1661 let src = "from docs where stage in [\"nigredo\"]";
1663 let e = oqx::parse_string(src).unwrap_err();
1664 let err = parse_invalid(src, &e, Some(&json!({ "query": src })));
1665 assert_eq!(
1666 err.message,
1667 "unexpected '[' — expected a value (at offset 25)"
1668 );
1669 assert!(err.data.unwrap().get("fix").is_none());
1670 }
1671
1672 #[test]
1673 fn row_functions_become_self_methods() {
1674 let q = oqx::parse_string(
1675 "from blocks where text(\"x\") && under_heading(\"h\") && size(_attrs) > 0 && _doc._path.startsWith(\"a\")",
1676 )
1677 .unwrap();
1678 let r = rewrite_query(&q, 0);
1679 let Some(Where::And { parts, .. }) = &r.r#where else {
1680 panic!("and")
1681 };
1682 let Where::Scalar { expr, .. } = &parts[0] else {
1683 panic!("scalar")
1684 };
1685 assert!(
1686 matches!(expr, Expr::Call { recv: Some(r), name, .. } if name == "text" && **r == build::ident("$it"))
1687 );
1688 let Where::Scalar { expr, .. } = &parts[2] else {
1689 panic!("scalar")
1690 };
1691 assert!(
1692 matches!(expr, Expr::Binary { left, .. } if matches!(&**left, Expr::Call { recv: None, name, .. } if name == "size"))
1693 );
1694 }
1695
1696 const NOW: i64 = 1_790_467_200_000;
1700
1701 fn where_expr(src: &str) -> Expr {
1702 let q = oqx::parse_string(&format!("from docs where {src}")).unwrap();
1703 let Some(Where::Scalar { expr, .. }) = q.r#where else {
1704 panic!("not a scalar where")
1705 };
1706 expr
1707 }
1708
1709 fn fold(src: &str) -> Expr {
1711 fn go(e: Expr) -> Expr {
1712 match e {
1713 Expr::Call {
1714 recv,
1715 name,
1716 args,
1717 span,
1718 } => fold_dates(
1719 Expr::Call {
1720 recv: recv.map(|r| Box::new(go(*r))),
1721 name,
1722 args: args.into_iter().map(go).collect(),
1723 span,
1724 },
1725 NOW,
1726 ),
1727 other => other,
1728 }
1729 }
1730 go(where_expr(src))
1731 }
1732
1733 fn lit(e: &Expr) -> Option<&Value> {
1734 match e {
1735 Expr::Lit { value, .. } => Some(value),
1736 _ => None,
1737 }
1738 }
1739
1740 #[test]
1741 fn folds_date_dur_over_literals_and_date_methods_over_literal_receivers_and_arguments() {
1742 assert_eq!(
1743 lit(&fold("date(\"2026-01-05\")")),
1744 Some(&Value::from("2026-01-05"))
1745 );
1746 assert_eq!(
1747 lit(&fold("date(\"today\")")),
1748 Some(&Value::from("2026-09-27"))
1749 );
1750 assert_eq!(
1751 lit(&fold("dur(\"30 days\")")),
1752 Some(&Value::from(30.0 * 86400.0))
1753 );
1754 assert_eq!(
1755 lit(&fold("date(\"today\").minus(dur(\"30 days\"))")),
1756 Some(&Value::from("2026-08-28"))
1757 );
1758 assert_eq!(lit(&fold("date(\"x\")")), Some(&Value::Null));
1759 assert_eq!(lit(&fold("date(5)")), Some(&Value::Null));
1760 assert_eq!(lit(&fold("dur(60).dur_text()")), Some(&Value::from("1m")));
1761 }
1762
1763 #[test]
1764 fn leaves_a_non_literal_argument_an_unknown_name_and_a_wrong_arity_in_place() {
1765 for src in [
1766 "date(when)",
1767 "date(\"today\").until(when)",
1768 "size(\"x\")",
1769 "date()",
1770 "date(\"2026-01-01\").plus()",
1771 "\"x\".lower()",
1772 ] {
1773 assert!(matches!(fold(src), Expr::Call { .. }), "{src}");
1774 }
1775 }
1776
1777 #[test]
1778 fn the_runner_folds_at_its_clock_so_a_named_point_pushes_down() {
1779 let q = oqx::parse_string("from docs where _updated_at < date(\"today\") && date(when) < date(\"today\").minus(dur(\"30 days\"))").unwrap();
1780 let printed = oqx::print_query(&rewrite_query(&q, NOW)).unwrap();
1781 assert!(
1782 printed.contains("_updated_at < \"2026-09-27\""),
1783 "{printed}"
1784 );
1785 assert!(printed.contains("date(when) < \"2026-08-28\""), "{printed}");
1786 }
1787
1788 #[test]
1789 fn path_literals_are_rooted_only_against_path_reads() {
1790 let q = oqx::parse_string(
1791 "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\"",
1792 )
1793 .unwrap();
1794 let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1795 assert!(printed.contains("_path.startsWith(\"/lab/\")"), "{printed}");
1796 assert!(printed.contains("_doc._path == \"/a.md\""), "{printed}");
1797 assert!(printed.contains("type == \"a.md\""), "{printed}");
1798 assert!(printed.contains("^_path == \"/b.md\""), "{printed}");
1799 assert!(printed.contains("\"/x.md\" != _dst_path"), "{printed}");
1800 assert!(printed.contains("_path == ^after"), "{printed}");
1803 assert!(printed.contains("path == \"c.md\""), "{printed}");
1804 let q = oqx::parse_string(
1806 "from docs where _path.contains(\"a/\") && _path.endsWith(\".md\") && _path == \"\"",
1807 )
1808 .unwrap();
1809 let printed = oqx::print_query(&rewrite_query(&q, 0)).unwrap();
1810 assert!(printed.contains("_path.contains(\"a/\")"), "{printed}");
1811 assert!(printed.contains("_path.endsWith(\".md\")"), "{printed}");
1812 assert!(printed.contains("_path == \"/\""), "{printed}");
1813 }
1814
1815 #[test]
1816 fn repo_and_bare_targets_are_refused_statically() {
1817 let parse = |s: &str| oqx::parse_string(s).unwrap();
1818 for q in [
1819 "from docs where $repo",
1820 "select r: size($repo.docs) from docs",
1821 "select r: $repo._id from docs",
1822 "select r: _nodes first { select x: ^$repo._id values } from docs",
1823 "select r: _nodes first { select x: 0^$repo values } from docs",
1824 "$repo.docs count { }",
1825 ] {
1826 let e = check_root_spellings(&parse(q)).unwrap_err();
1827 assert_eq!(e.code, "filter_invalid", "{q}");
1828 assert_eq!(e.message, REPO_REMOVED_MESSAGE, "{q}");
1829 }
1830 let e = check_root_spellings(&parse("select x: docs collect { } from docs")).unwrap_err();
1833 assert_eq!(e.message, bare_target_message(Target::Docs, Some(1)));
1834 assert!(
1835 e.message
1836 .contains("did you mean `^docs` (the repository's documents)?")
1837 );
1838 let e = check_root_spellings(&parse(
1839 "select x: _nodes collect { select y: size(links) } from docs",
1840 ))
1841 .unwrap_err();
1842 assert!(
1843 e.message
1844 .contains("did you mean `^^links` (the repository's links)?")
1845 );
1846 let e = check_root_spellings(&parse("docs collect { from docs }")).unwrap_err();
1847 assert!(e.message.contains("`^docs`"));
1848 let e = check_root_spellings(&parse("from docs where nodes exists { }")).unwrap_err();
1851 assert_eq!(e.message, bare_target_message(Target::Nodes, Some(1)));
1852 let e = check_root_spellings(&parse("from blocks where blocks exists { }")).unwrap_err();
1853 assert_eq!(e.message, bare_target_message(Target::Blocks, Some(1)));
1854 let e =
1855 check_root_spellings(&parse("select r: revisions count { } from docs")).unwrap_err();
1856 assert_eq!(e.message, bare_target_message(Target::Revisions, Some(1)));
1857 for q in [
1859 "docs count { }",
1860 "from docs where _nodes exists { }",
1861 "entries(docs) first { }",
1862 "select n: size(^docs), i: ^_id, k: size(0^links) from docs",
1863 "from blocks where _nodes exists { where ^blocks exists { } }",
1864 ] {
1865 check_root_spellings(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1866 }
1867 let e = check_root_spellings(&parse("^docs count { }")).unwrap_err();
1869 assert_eq!(e.message, past_root_message(Target::Docs, 1));
1870 assert_eq!(
1871 e.message,
1872 "`^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)`)"
1873 );
1874 let e = check_root_spellings(&parse("select _path from docs where ^^links exists { }"))
1875 .unwrap_err();
1876 assert!(
1877 e.message.contains("`^^links` reaches past the root"),
1878 "{}",
1879 e.message
1880 );
1881 let m = bare_target_message(Target::Blocks, None);
1883 assert!(m.contains("did you mean `^blocks` (the repository's blocks; one caret per enclosing block, or the absolute `0^blocks`)?"), "{m}");
1884 }
1885
1886 #[test]
1887 fn reserved_projection_names_are_refused() {
1888 let parse = |s: &str| oqx::parse_string(s).unwrap();
1889 let e = check_reserved_aliases(&parse("select __oqx_id: 1 from docs")).unwrap_err();
1890 assert_eq!(e.code, "filter_invalid");
1891 assert_eq!(
1892 e.message,
1893 "projection `__oqx_id` uses the reserved prefix `__oqx_` — choose another alias"
1894 );
1895 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1896 let e = check_reserved_aliases(&parse("select __omg_sel_0_x: 1 from docs")).unwrap_err();
1897 assert!(
1898 e.message.contains("reserved prefix `__omg_`"),
1899 "{}",
1900 e.message
1901 );
1902 assert!(
1904 check_reserved_aliases(&parse(
1905 "select __oqx: 1, x: _nodes collect { select __oqx_y: 1 } from docs"
1906 ))
1907 .is_ok()
1908 );
1909 }
1910
1911 #[test]
1912 fn semantic_phrases_are_collected_distinct() {
1913 let phrases = collect_semantic_phrases(
1914 "select s: semantic(\"alpha\") from docs where semantic(\"alpha\") > 0.5 || _nodes exists { where semantic(\"beta\") > 0 } order by semantic(\"gamma\") desc",
1915 );
1916 assert_eq!(phrases, ["alpha", "beta", "gamma"]);
1917 assert!(collect_semantic_phrases("not a query {{").is_empty());
1918 assert!(collect_semantic_phrases("from docs").is_empty());
1919 }
1920
1921 #[test]
1922 fn hits_peel_the_injected_columns() {
1923 let mut o = oqx::Object::new();
1924 o.insert(ID_KEY, Value::Str("d_1".into()));
1925 o.insert(PATH_KEY, Value::Null);
1926 o.insert("layer", Value::Str("canon".into()));
1927 let shaped = shape_row(Value::Object(o)).unwrap();
1928 assert_eq!((shaped.id.as_str(), shaped.path.as_str()), ("d_1", ""));
1929 let hit = to_hit(shaped);
1930 let ho = hit.as_object().unwrap();
1931 assert_eq!(ho.keys().collect::<Vec<_>>(), ["_id", "_path", "layer"]);
1932 assert_eq!(ho.get("_path"), Some(&Value::Str(String::new())));
1933 let mut o = oqx::Object::new();
1936 o.insert(ID_KEY, Value::Str("d_1".into()));
1937 o.insert(PATH_KEY, Value::Str("a.md".into()));
1938 o.insert("_id", Value::Number(7.0));
1939 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1940 let ho = hit.as_object().unwrap();
1941 assert_eq!(ho.keys().collect::<Vec<_>>(), ["_id", "_path"]);
1942 assert_eq!(ho.get("_id"), Some(&Value::Number(7.0)));
1943 let mut o = oqx::Object::new();
1945 o.insert(ID_KEY, Value::Str("d_1".into()));
1946 o.insert(PATH_KEY, Value::Str("a.md".into()));
1947 o.insert("path", Value::Str("authored".into()));
1948 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1949 assert_eq!(
1950 hit.as_object().unwrap().keys().collect::<Vec<_>>(),
1951 ["_id", "_path", "path"]
1952 );
1953 let mut o = oqx::Object::new();
1955 o.insert(ID_KEY, Value::Str("d_1".into()));
1956 o.insert(PATH_KEY, Value::Undefined);
1957 let row = Value::Object(o);
1958 assert_eq!(raw_identity(&row), (String::new(), "d_1".to_owned()));
1959 let shaped = shape_row(row).unwrap();
1960 assert_eq!((shaped.path, shaped.id), (String::new(), "d_1".to_owned()));
1961 }
1962
1963 #[test]
1964 fn bare_hits_are_the_projection_in_its_own_order() {
1965 let mut o = oqx::Object::new();
1968 o.insert(ID_KEY, Value::Str("d_1".into()));
1969 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1970 o.insert("_title", Value::Str("T".into()));
1971 o.insert("_id", Value::Str("d_1".into()));
1972 let shaped = shape_row(Value::Object(o)).unwrap();
1973 let bare = to_bare_hit(shaped);
1974 let bo = bare.as_object().unwrap();
1975 assert_eq!(bo.keys().collect::<Vec<_>>(), ["_title", "_id"]);
1976 let mut o = oqx::Object::new();
1979 o.insert(ID_KEY, Value::Str("d_1".into()));
1980 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1981 o.insert("_path", Value::Str("canon".into()));
1982 let shaped = shape_row(Value::Object(o)).unwrap();
1983 assert_eq!(shaped.path, "/a.md");
1984 assert_eq!(
1985 render_hit(shaped, true),
1986 serde_json::json!({ "_path": "canon" })
1987 );
1988 let mut o = oqx::Object::new();
1990 o.insert(ID_KEY, Value::Str("d_1".into()));
1991 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1992 o.insert("_path", Value::Str("/a.md".into()));
1993 assert_eq!(
1994 render_hit(shape_row(Value::Object(o)).unwrap(), false),
1995 serde_json::json!({ "_id": "d_1", "_path": "/a.md" })
1996 );
1997 }
1998
1999 #[test]
2000 fn bare_needs_a_select_and_is_not_the_values_form() {
2001 let parse = |s: &str| oqx::parse_string(s).unwrap();
2002 for q in ["from docs", "from docs where layer == \"canon\""] {
2003 let e = check_bare(&parse(q)).unwrap_err();
2004 assert_eq!(e.code, "filter_invalid", "{q}");
2005 assert_eq!(e.message, BARE_NEEDS_SELECT_MESSAGE, "{q}");
2006 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
2007 }
2008 let e = check_bare(&parse("select _path values from docs")).unwrap_err();
2009 assert_eq!(e.code, "filter_invalid");
2010 assert_eq!(e.message, BARE_WITH_VALUES_MESSAGE);
2011 for q in [
2012 "select _path from docs",
2013 "select _id, _title from docs",
2014 "docs first { select t: _title }",
2015 ] {
2016 check_bare(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
2017 }
2018 }
2019
2020 #[test]
2021 fn a_projection_onto_the_envelope_must_be_aliased() {
2022 let items = |src: &str| oqx::parse_string(src).unwrap().select;
2023 assert!(envelope_collision(&items("select _id, _path from docs")).is_none());
2025 assert!(
2026 envelope_collision(&items("select _id: _id, _path: _path, p: _path from docs"))
2027 .is_none()
2028 );
2029 assert!(envelope_collision(&items("select id, path, d: _data.path from docs")).is_none());
2031 let e = envelope_collision(&items("select _path: path from docs")).unwrap();
2033 assert_eq!(e.code, "filter_invalid");
2034 assert_eq!(
2035 e.message,
2036 "projection `_path` collides with the hit envelope — alias it (`p: …`)"
2037 );
2038 let e = envelope_collision(&items("select _id: layer from docs")).unwrap();
2039 assert_eq!(
2040 e.message,
2041 "projection `_id` collides with the hit envelope — alias it (`i: …`)"
2042 );
2043 assert!(envelope_collision(&items("select _path: _doc._path from blocks")).is_some());
2044 assert!(envelope_collision(&items("select _path: _nodes collect { } from docs")).is_some());
2045 }
2046
2047 #[test]
2048 fn a_flatten_bound_onto_the_envelope_is_refused() {
2049 let flatten = |src: &str| oqx::parse_string(src).unwrap().flatten;
2050 assert!(
2052 flatten_collision(&flatten("select _path, t from docs flatten _tags as t")).is_none()
2053 );
2054 assert!(
2055 flatten_collision(&flatten(
2056 "select _path, b from docs flatten _blocks { where _type == \"task\" } as b flatten b._children as c"
2057 ))
2058 .is_none()
2059 );
2060 assert!(flatten_collision(&flatten("select _path from docs")).is_none());
2061 assert!(
2063 flatten_collision(&flatten("select _path from docs flatten _tags as id")).is_none()
2064 );
2065 let e = flatten_collision(&flatten("select _path from docs flatten _tags as _id")).unwrap();
2067 assert_eq!(e.code, "filter_invalid");
2068 assert_eq!(
2069 e.message,
2070 "flatten `as _id` collides with the hit envelope — a flattened row is still the row, whose `_id` the envelope reads; bind another name"
2071 );
2072 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
2073 let e = flatten_collision(&flatten(
2074 "select _title from docs flatten _tags as t flatten paths as _path",
2075 ))
2076 .unwrap();
2077 assert_eq!(
2078 e.message,
2079 "flatten `as _path` collides with the hit envelope — a flattened row is still the row, whose `_path` the envelope reads; bind another name"
2080 );
2081 }
2082
2083 #[test]
2084 fn a_path_read_is_the_attribute_or_a_row_handle_never_a_source_handle() {
2085 let pred = |src: &str| match oqx::parse_string(src).unwrap().r#where.unwrap() {
2086 oqx::ast::Where::Scalar { expr, .. } => expr,
2087 other => panic!("{other:?}"),
2088 };
2089 let left = |e: Expr| match e {
2090 Expr::Binary { left, .. } => *left,
2091 other => panic!("{other:?}"),
2092 };
2093 assert!(is_path_read(&left(pred("from docs where _path == \"x\""))));
2094 assert!(is_path_read(&left(pred(
2095 "from blocks where _doc._path == \"x\""
2096 ))));
2097 assert!(is_path_read(&left(pred("from docs where ^_path == \"x\""))));
2098 assert!(is_path_read(&left(pred(
2099 "from links where _dst_path == \"x\""
2100 ))));
2101 assert!(is_path_read(&left(pred(
2102 "from links where _block._path == \"x\""
2103 ))));
2104 assert!(!is_path_read(&left(pred("from docs where path == \"x\""))));
2106 assert!(!is_path_read(&left(pred(
2107 "from docs where _data.path == \"x\""
2108 ))));
2109 assert!(!is_path_read(&left(pred(
2110 "from docs where _frontmatter.path == \"x\""
2111 ))));
2112 }
2113
2114 #[test]
2115 fn a_hit_that_is_not_a_store_row_fails_the_query() {
2116 let mut o = oqx::Object::new();
2119 o.insert(ID_KEY, Value::Undefined);
2120 o.insert(PATH_KEY, Value::Undefined);
2121 o.insert(SELF_KEY, Value::Str("/timeline/kickoff.md".into()));
2122 let e = shape_row(Value::Object(o)).unwrap_err();
2123 assert_eq!(e.code, "filter_invalid");
2124 assert_eq!(
2125 e.message,
2126 "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>)"
2127 );
2128 let mut row = oqx::Object::new();
2130 row.insert("doc_id", Value::Str("d_1".into()));
2131 row.insert("path", Value::Str("a.md".into()));
2132 let row = crate::context::tag_row(row, crate::context::Target::Docs);
2133 let mut o = oqx::Object::new();
2134 o.insert(ID_KEY, Value::Str("d_1".into()));
2135 o.insert(PATH_KEY, Value::Str("a.md".into()));
2136 o.insert(SELF_KEY, row);
2137 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
2138 assert_eq!(
2139 hit.as_object().unwrap().keys().collect::<Vec<_>>(),
2140 ["_id", "_path"]
2141 );
2142 let parse = |s: &str| oqx::parse_string(s).unwrap();
2144 assert!(!may_reach_non_rows(&parse(
2145 "from docs where layer == \"canon\""
2146 )));
2147 assert!(may_reach_non_rows(&parse("from docs follow before")));
2148 assert!(may_reach_non_rows(&parse("refs(\"/index.md\") first { }")));
2149 }
2150
2151 #[test]
2152 fn distinct_dedups_by_user_projection_first_wins() {
2153 let mk = |id: &str, t: &str| {
2154 let mut proj = oqx::Object::new();
2155 proj.insert("type", Value::Str(t.into()));
2156 Shaped {
2157 id: id.to_owned(),
2158 path: "p".to_owned(),
2159 proj,
2160 }
2161 };
2162 let out = dedup_by_projection(vec![mk("1", "a"), mk("2", "b"), mk("3", "a")], false);
2163 assert_eq!(out.len(), 2);
2164 assert_eq!(out[0].id, "1");
2165 assert_eq!(out[1].id, "2");
2166 let with_id = |id: &str| {
2170 let mut proj = oqx::Object::new();
2171 proj.insert("_id", Value::Str(id.into()));
2172 Shaped {
2173 id: id.to_owned(),
2174 path: "p".to_owned(),
2175 proj,
2176 }
2177 };
2178 assert_eq!(
2179 dedup_by_projection(vec![with_id("1"), with_id("2")], false).len(),
2180 1
2181 );
2182 assert_eq!(
2183 dedup_by_projection(vec![with_id("1"), with_id("2")], true).len(),
2184 2
2185 );
2186 }
2187
2188 #[test]
2189 fn the_page_runs_on_identity() {
2190 let rows: Vec<(String, String)> = [("/a.md", "d_1"), ("/a.md", "d_2"), ("/b.md", "d_3")]
2191 .iter()
2192 .map(|(p, i)| ((*p).to_owned(), (*i).to_owned()))
2193 .collect();
2194 let ident = |r: &(String, String)| (r.0.clone(), r.1.clone());
2195 let (page, truncated, next) =
2197 page_rows(rows.clone(), ident, 0, None, None, 2, false).unwrap();
2198 assert_eq!(page.len(), 2);
2199 assert!(truncated);
2200 let next = next.expect("a cursor");
2201 assert_eq!(next, encode_cursor(&["/a.md", "d_2"]));
2202 let (page, truncated, next) =
2204 page_rows(rows.clone(), ident, 0, None, Some(&next), 2, false).unwrap();
2205 assert_eq!(page, vec![("/b.md".to_owned(), "d_3".to_owned())]);
2206 assert!(!truncated && next.is_none());
2207 let (page, truncated, next) =
2209 page_rows(rows.clone(), ident, 1, Some(1), None, 50, true).unwrap();
2210 assert_eq!(page, vec![("/a.md".to_owned(), "d_2".to_owned())]);
2211 assert!(!truncated && next.is_none());
2212 let (page, truncated, next) = page_rows(rows, ident, 0, None, None, 1, true).unwrap();
2213 assert_eq!(page.len(), 1);
2214 assert!(truncated && next.is_none());
2215 }
2216
2217 #[test]
2218 fn top_level_bounds_must_be_literals() {
2219 assert_eq!(const_bound(None, "limit").unwrap(), None);
2220 assert_eq!(
2221 const_bound(
2222 Some(&Expr::Lit {
2223 value: Value::Number(3.0),
2224 span: oqx::Span::EMPTY
2225 }),
2226 "limit"
2227 )
2228 .unwrap(),
2229 Some(3)
2230 );
2231 let e = const_bound(
2232 Some(&Expr::Lit {
2233 value: Value::Number(-1.0),
2234 span: oqx::Span::EMPTY,
2235 }),
2236 "offset",
2237 )
2238 .unwrap_err();
2239 assert_eq!(e.code, "filter_invalid");
2240 assert!(e.message.contains("top-level offset"));
2241 }
2242}