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