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