1use std::collections::HashMap;
13
14use omgbase_search::{EmbeddingProvider, f32_to_blob};
15use omgbase_store::Store;
16use oqx::ast::{BinaryOp, Expr, 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::error::{Result, SurfaceError};
24use crate::names::{UnknownNames, VocabularyCache, check_names};
25use crate::paths::reference_path;
26use crate::planner::{SqlitePlanner, root_target};
27
28pub const DEFAULT_LIMIT: usize = 50;
30
31const ROW_FNS: [&str; 12] = [
37 "text",
38 "semantic",
39 "under",
40 "under_heading",
41 "within",
42 "under_kind",
43 "yaml_path",
44 "json_pointer",
45 "has_edge",
46 "has_anchor",
47 "child_count",
48 "parent_type",
49];
50
51const ID_KEY: &str = "__oqx_id";
52const PATH_KEY: &str = "__oqx_path";
53pub(crate) const HIT_ID: &str = "id";
59pub(crate) const HIT_PATH: &str = "path";
60const SELF_KEY: &str = "__oqx_self";
70const VALUE_KEY: &str = "__oqx_value";
74
75#[derive(Clone, Copy, Default)]
77pub struct QueryOptions<'a> {
78 pub limit: Option<usize>,
80 pub cursor: Option<&'a str>,
82 pub provider: Option<&'a dyn EmbeddingProvider>,
85 pub in_memory: bool,
88 pub bare: bool,
96 pub unknown_names: Option<UnknownNames>,
104 pub vocabulary: Option<&'a VocabularyCache>,
107}
108
109#[derive(Clone, Debug, PartialEq)]
111pub struct OqxResult {
112 pub hits: Vec<Json>,
113 pub truncated: bool,
114 pub cursor: Option<String>,
115 pub consumer: Consumer,
116 pub count: Option<f64>,
117 pub exists: Option<bool>,
118 pub none: Option<bool>,
119 pub values: Option<Vec<Json>>,
121 pub hints: Vec<String>,
127}
128
129impl OqxResult {
130 fn scalar(consumer: Consumer) -> Self {
131 Self {
132 hits: Vec::new(),
133 truncated: false,
134 cursor: None,
135 consumer,
136 count: None,
137 exists: None,
138 none: None,
139 values: None,
140 hints: Vec::new(),
141 }
142 }
143
144 #[must_use]
147 pub fn to_json(&self) -> Json {
148 let mut m = Map::new();
149 m.insert("hits".to_owned(), Json::Array(self.hits.clone()));
150 m.insert("truncated".to_owned(), Json::Bool(self.truncated));
151 m.insert(
152 "cursor".to_owned(),
153 self.cursor.clone().map_or(Json::Null, Json::String),
154 );
155 m.insert(
156 "consumer".to_owned(),
157 Json::String(self.consumer.as_str().to_owned()),
158 );
159 if let Some(n) = self.count {
160 m.insert("count".to_owned(), Value::Number(n).to_canonical_json());
161 }
162 if let Some(b) = self.exists {
163 m.insert("exists".to_owned(), Json::Bool(b));
164 }
165 if let Some(b) = self.none {
166 m.insert("none".to_owned(), Json::Bool(b));
167 }
168 if let Some(v) = &self.values {
169 m.insert("values".to_owned(), Json::Array(v.clone()));
170 }
171 if !self.hints.is_empty() {
172 m.insert(
173 "hints".to_owned(),
174 Json::Array(self.hints.iter().cloned().map(Json::String).collect()),
175 );
176 }
177 Json::Object(m)
178 }
179}
180
181const PATH_ATTRIBUTES: [&str; 2] = ["path", "dst_path"];
192const ROW_HANDLES: [&str; 4] = ["doc", "block", "dst", "$it"];
197
198fn is_path_read(e: &Expr) -> bool {
199 match e {
200 Expr::Ident { name, .. } | Expr::Outer { name, .. } => {
201 PATH_ATTRIBUTES.contains(&name.as_str())
202 }
203 Expr::Member { recv, name, .. } if PATH_ATTRIBUTES.contains(&name.as_str()) => {
204 matches!(&**recv, Expr::Ident { name, .. } | Expr::Outer { name, .. } if ROW_HANDLES.contains(&name.as_str()))
205 }
206 _ => false,
207 }
208}
209
210fn envelope_collision(items: &[SelectItem]) -> Option<SurfaceError> {
221 for item in items {
222 let name = match item {
223 SelectItem::Field { name, .. } | SelectItem::Collect { name, .. } => name.as_str(),
224 };
225 if name != HIT_ID && name != HIT_PATH {
226 continue;
227 }
228 if let SelectItem::Field { expr, .. } = item
229 && matches!(expr, Expr::Ident { name: n, .. } if n == name)
230 {
231 continue;
232 }
233 let hint = if name == HIT_PATH {
234 "p: data.path"
235 } else {
236 "i: data.id"
237 };
238 return Some(SurfaceError::filter_invalid(
239 format!("projection `{name}` collides with the hit envelope — alias it (`{hint}`)"),
240 "OQX",
241 ));
242 }
243 None
244}
245
246fn root_literal(e: Expr) -> Expr {
247 match e {
248 Expr::Lit {
249 value: Value::Str(s),
250 span,
251 } => Expr::Lit {
252 value: Value::Str(reference_path(&s)),
253 span,
254 },
255 other => other,
256 }
257}
258
259#[must_use]
261pub fn root_path_literals(e: Expr) -> Expr {
262 match e {
263 Expr::Binary {
264 op: op @ (BinaryOp::Eq | BinaryOp::Ne),
265 left,
266 right,
267 span,
268 } => {
269 if is_path_read(&left) {
270 Expr::Binary {
271 op,
272 left,
273 right: Box::new(root_literal(*right)),
274 span,
275 }
276 } else if is_path_read(&right) {
277 Expr::Binary {
278 op,
279 left: Box::new(root_literal(*left)),
280 right,
281 span,
282 }
283 } else {
284 Expr::Binary {
285 op,
286 left,
287 right,
288 span,
289 }
290 }
291 }
292 Expr::Call {
293 recv: Some(recv),
294 name,
295 mut args,
296 span,
297 } if name == "startsWith" && args.len() == 1 && is_path_read(&recv) => {
298 let arg = root_literal(args.remove(0));
299 Expr::Call {
300 recv: Some(recv),
301 name,
302 args: vec![arg],
303 span,
304 }
305 }
306 other => other,
307 }
308}
309
310#[must_use]
314pub fn rewrite_query(q: &Query) -> Query {
315 transform(q, &mut |e, _| match e {
316 Expr::Call {
317 recv: None,
318 name,
319 args,
320 span,
321 } if ROW_FNS.contains(&name.as_str()) => Expr::Call {
322 recv: Some(Box::new(build::ident("$self"))),
323 name,
324 args,
325 span,
326 },
327 other => root_path_literals(other),
328 })
329}
330
331pub 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`";
335
336#[must_use]
340pub fn bare_target_message(t: Target, depth: Option<usize>) -> String {
341 let name = t.as_str();
342 let noun = t.noun();
343 let carets = "^".repeat(depth.unwrap_or(1));
344 let how = if depth.is_none() {
345 format!("; one caret per enclosing block, or the absolute `0^{name}`")
346 } else {
347 String::new()
348 };
349 format!(
350 "`{name}` inside a block reads a property of the current row, which has none — did you mean `{carets}{name}` (the repository's {noun}{how})?"
351 )
352}
353
354#[must_use]
360pub fn unknown_target_message(name: &str) -> String {
361 let was = if name == "edges" {
362 " (`edges` was renamed `links` in surface 3.0)"
363 } else {
364 ""
365 };
366 let roots: Vec<String> = crate::context::ALL_TARGETS
367 .iter()
368 .map(|t| format!("`{}`", t.as_str()))
369 .collect();
370 format!(
371 "`{name}` is not a target — the repository's collections are {}{was}",
372 roots.join(", ")
373 )
374}
375
376#[must_use]
380pub fn past_root_message(t: Target, levels: usize) -> String {
381 let name = t.as_str();
382 let noun = t.noun();
383 let carets = "^".repeat(levels);
384 format!(
385 "`{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})`)"
386 )
387}
388
389pub fn check_root_spellings(q: &Query) -> Result<()> {
403 if let Expr::Ident { name, .. } = &q.source {
407 if Target::parse(name).is_none() {
408 return Err(SurfaceError::filter_invalid(
409 unknown_target_message(name),
410 "OQX",
411 ));
412 }
413 }
414 struct Check(Option<SurfaceError>);
415 impl Visitor<'_> for Check {
416 fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
417 if self.0.is_some() {
418 return false;
419 }
420 match node {
421 Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. })
422 if name == "$repo" =>
423 {
424 self.0 = Some(SurfaceError::filter_invalid(REPO_REMOVED_MESSAGE, "OQX"));
425 }
426 Node::Expr(Expr::Ident { name, .. })
427 if ctx.depth >= 1 && matches!(name.as_str(), "docs" | "links") =>
428 {
429 let t = Target::parse(name).expect("a target name");
430 self.0 = Some(SurfaceError::filter_invalid(
431 bare_target_message(t, Some(ctx.depth)),
432 "OQX",
433 ));
434 }
435 Node::Expr(Expr::Outer { name, levels, .. }) if *levels > ctx.depth => {
436 if let Some(t) = Target::parse(name) {
437 self.0 = Some(SurfaceError::filter_invalid(
438 past_root_message(t, *levels),
439 "OQX",
440 ));
441 }
442 }
443 _ => {}
444 }
445 self.0.is_none()
446 }
447 }
448 let mut check = Check(None);
449 visit(Node::Query(q), &mut check);
450 check.0.map_or(Ok(()), Err)
451}
452
453struct Phrases(Vec<String>);
457
458impl Visitor<'_> for Phrases {
459 fn enter(&mut self, node: Node<'_>, _ctx: &VisitContext<'_>) -> bool {
460 if let Node::Expr(Expr::Call {
461 recv: None,
462 name,
463 args,
464 ..
465 }) = node
466 && name == "semantic"
467 && let Some(Expr::Lit {
468 value: Value::Str(s),
469 ..
470 }) = args.first()
471 && !self.0.contains(s)
472 {
473 self.0.push(s.clone());
474 }
475 true
476 }
477}
478
479#[must_use]
482pub fn collect_semantic_phrases(source: &str) -> Vec<String> {
483 let Ok(q) = oqx::parse_string(source) else {
484 return Vec::new();
485 };
486 semantic_phrases_of(&q)
487}
488
489#[must_use]
491pub fn semantic_phrases_of(q: &Query) -> Vec<String> {
492 let mut phrases = Phrases(Vec::new());
493 visit(Node::Query(q), &mut phrases);
494 phrases.0
495}
496
497pub fn embed_phrases(
504 provider: Option<&dyn EmbeddingProvider>,
505 phrases: Vec<String>,
506) -> Result<HashMap<String, SemanticVec>> {
507 let mut semantic: HashMap<String, SemanticVec> = HashMap::new();
508 if let Some(provider) = provider.filter(|_| !phrases.is_empty()) {
509 for phrase in phrases {
510 let vec = provider
511 .embed_query(&phrase)
512 .map_err(|e| SurfaceError::new(e.code(), e.to_string()))?;
513 semantic.insert(
514 phrase,
515 SemanticVec {
516 model: provider.model().to_owned(),
517 vec: f32_to_blob(&vec),
518 },
519 );
520 }
521 }
522 Ok(semantic)
523}
524
525pub const RESERVED_ALIAS_PREFIXES: [&str; 2] = ["__oqx_", "__omg_"];
536
537pub fn check_reserved_aliases(q: &Query) -> Result<()> {
539 for item in &q.select {
540 if let Some(prefix) = RESERVED_ALIAS_PREFIXES
541 .iter()
542 .find(|p| item.name().starts_with(*p))
543 {
544 return Err(SurfaceError::filter_invalid(
545 format!(
546 "projection `{}` uses the reserved prefix `{prefix}` — choose another alias",
547 item.name()
548 ),
549 "OQX",
550 ));
551 }
552 }
553 Ok(())
554}
555
556fn mentions_outside_source(q: &Query, name: &str) -> bool {
562 struct Mentions<'n> {
563 name: &'n str,
564 found: bool,
565 }
566 impl Visitor<'_> for Mentions<'_> {
567 fn enter(&mut self, node: Node<'_>, ctx: &VisitContext<'_>) -> bool {
568 if self.found {
569 return false;
570 }
571 if ctx.clause == Some(Clause::Source) && ctx.path.len() == 1 {
573 return false;
574 }
575 if let Node::Expr(Expr::Ident { name, .. } | Expr::Outer { name, .. }) = node
576 && name == self.name
577 {
578 self.found = true;
579 }
580 !self.found
581 }
582 }
583 let mut m = Mentions { name, found: false };
584 visit(Node::Query(q), &mut m);
585 m.found
586}
587
588fn may_reach_non_rows(q: &Query) -> bool {
596 q.follow.is_some() || !q.from.is_empty() || root_target(&q.source).is_none()
597}
598
599fn describe_value(v: &Value) -> String {
601 match v {
602 Value::Undefined | Value::Null => "an absent value".to_owned(),
603 Value::Str(s) => format!(
604 "a string ({})",
605 serde_json::to_string(s).unwrap_or_default()
606 ),
607 Value::Number(_) => format!("a number ({v})"),
608 Value::Bool(b) => format!("a boolean ({b})"),
609 Value::Array(_) => "an array".to_owned(),
610 Value::Range(_) => "a range".to_owned(),
611 Value::Object(_) => "an object".to_owned(),
612 }
613}
614
615fn not_a_store_row(v: &Value) -> SurfaceError {
616 SurfaceError::filter_invalid(
617 format!(
618 "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>)",
619 describe_value(v)
620 ),
621 "OQX",
622 )
623}
624
625#[derive(Debug)]
635struct Shaped {
636 id: String,
637 path: String,
638 proj: oqx::Object,
639}
640
641fn path_string(path: &Value) -> String {
642 if path.is_absent() {
643 String::new()
644 } else {
645 path.to_string()
646 }
647}
648
649fn shape_row(row: Value) -> Result<Shaped> {
650 let mut shaped = Shaped {
651 id: Value::Undefined.to_string(),
652 path: String::new(),
653 proj: oqx::Object::new(),
654 };
655 let Value::Object(mut o) = row else {
656 return Ok(shaped);
657 };
658 if let Some(me) = o.remove(SELF_KEY)
659 && target_of(&me).is_none()
660 {
661 return Err(not_a_store_row(&me));
662 }
663 let Value::Object(o) = render_row_values(Value::Object(o)) else {
664 return Ok(shaped);
665 };
666 for (k, v) in o {
667 match k.as_str() {
668 ID_KEY => shaped.id = v.to_string(),
669 PATH_KEY => shaped.path = path_string(&v),
670 _ => {
671 shaped.proj.insert(k, v);
672 }
673 }
674 }
675 Ok(shaped)
676}
677
678fn raw_identity(row: &Value) -> (String, String) {
683 let get = |k: &str| {
684 row.as_object()
685 .and_then(|o| o.get(k))
686 .cloned()
687 .unwrap_or(Value::Undefined)
688 };
689 (path_string(&get(PATH_KEY)), get(ID_KEY).to_string())
690}
691
692fn to_hit(s: Shaped) -> Value {
697 let mut hit = oqx::Object::with_capacity(s.proj.len() + 2);
698 hit.insert(HIT_ID, Value::Str(s.id));
699 hit.insert(HIT_PATH, Value::Str(s.path));
700 for (k, v) in s.proj {
701 hit.insert(k, v);
702 }
703 Value::Object(hit)
704}
705
706fn to_bare_hit(s: Shaped) -> Value {
711 Value::Object(s.proj)
712}
713
714fn render_hit(s: Shaped, bare: bool) -> Json {
716 if bare {
717 to_bare_hit(s).to_canonical_json()
718 } else {
719 to_hit(s).to_canonical_json()
720 }
721}
722
723fn dedup_by_projection(rows: Vec<Shaped>, bare: bool) -> Vec<Shaped> {
732 let mut seen: Vec<String> = Vec::new();
733 let mut out = Vec::new();
734 for s in rows {
735 let mut pairs: Vec<(String, Value)> = s
736 .proj
737 .iter()
738 .filter(|(k, _)| bare || (*k != HIT_ID && *k != HIT_PATH))
739 .map(|(k, v)| (k.to_owned(), v.clone()))
740 .collect();
741 pairs.sort_by(|a, b| a.0.cmp(&b.0));
742 let key = Value::Array(
743 pairs
744 .into_iter()
745 .map(|(k, v)| Value::Array(vec![Value::Str(k), v]))
746 .collect(),
747 )
748 .to_canonical_json()
749 .to_string();
750 if seen.contains(&key) {
751 continue;
752 }
753 seen.push(key);
754 out.push(s);
755 }
756 out
757}
758
759fn const_bound(e: Option<&Expr>, word: &str) -> Result<Option<usize>> {
762 match e {
763 None => Ok(None),
764 Some(Expr::Lit {
765 value: Value::Number(n),
766 ..
767 }) if n.fract() == 0.0 && *n >= 0.0 && n.is_finite() => Ok(Some(*n as usize)),
768 Some(_) => Err(SurfaceError::filter_invalid(
769 format!("top-level {word} must be a non-negative integer literal"),
770 "OQX",
771 )),
772 }
773}
774
775fn page_rows<T>(
781 mut rows: Vec<T>,
782 ident: impl Fn(&T) -> (String, String),
783 offset: usize,
784 limit: Option<usize>,
785 cursor: Option<&str>,
786 cap: usize,
787 custom: bool,
788) -> Result<(Vec<T>, bool, Option<String>)> {
789 if offset > 0 || limit.is_some() {
790 let end = limit.map_or(rows.len(), |l| (offset + l).min(rows.len()));
791 if offset >= rows.len() {
792 rows.clear();
793 } else {
794 rows.truncate(end);
795 rows.drain(..offset);
796 }
797 }
798 if let Some(cursor) = cursor {
799 let parts = decode_path_cursor(cursor, "query", 2)?;
801 let (path, id) = (&parts[0], &parts[1]);
802 rows.retain(|r| {
803 let (rp, ri) = ident(r);
804 rp > *path || (rp == *path && ri > *id)
805 });
806 }
807 let truncated = rows.len() > cap;
808 rows.truncate(cap);
809 let next = if truncated && !custom {
810 rows.last().map(|last| {
811 let (p, i) = ident(last);
812 encode_cursor(&[&p, &i])
813 })
814 } else {
815 None
816 };
817 Ok((rows, truncated, next))
818}
819
820fn value_of(s: &Shaped) -> Json {
821 s.proj
822 .get(VALUE_KEY)
823 .cloned()
824 .unwrap_or(Value::Undefined)
825 .to_canonical_json()
826}
827
828pub fn check_bare(q: &Query) -> Result<()> {
835 if q.values {
836 return Err(SurfaceError::filter_invalid(
837 BARE_WITH_VALUES_MESSAGE,
838 "OQX",
839 ));
840 }
841 if q.select.is_empty() {
842 return Err(SurfaceError::filter_invalid(
843 BARE_NEEDS_SELECT_MESSAGE,
844 "OQX",
845 ));
846 }
847 Ok(())
848}
849
850pub const BARE_NEEDS_SELECT_MESSAGE: &str =
852 "`bare` needs a `select` — without a projection a hit is only its envelope";
853pub const BARE_WITH_VALUES_MESSAGE: &str =
854 "`bare` is redundant with `values` — `values` is already the bare-values form; drop one";
855
856pub fn query(
860 store: &Store,
861 repo_id: &str,
862 source: &str,
863 opts: QueryOptions<'_>,
864) -> Result<OqxResult> {
865 let semantic = embed_phrases(opts.provider, collect_semantic_phrases(source))?;
866 let runner = Runner {
867 store,
868 repo_id,
869 semantic,
870 planned: !opts.in_memory,
871 };
872 let raw = oqx::parse_string(source)?;
873 check_reserved_aliases(&raw)?;
874 run_parsed(
876 &runner,
877 &raw,
878 opts,
879 opts.unknown_names.unwrap_or(UnknownNames::Hint),
880 )
881}
882
883pub fn query_ast(
892 store: &Store,
893 repo_id: &str,
894 raw: &Query,
895 opts: QueryOptions<'_>,
896 semantic: HashMap<String, SemanticVec>,
897) -> Result<OqxResult> {
898 let runner = Runner {
899 store,
900 repo_id,
901 semantic,
902 planned: !opts.in_memory,
903 };
904 run_parsed(
905 &runner,
906 raw,
907 opts,
908 opts.unknown_names.unwrap_or(UnknownNames::Refuse),
909 )
910}
911
912struct Runner<'a> {
918 store: &'a Store,
919 repo_id: &'a str,
920 semantic: HashMap<String, SemanticVec>,
921 planned: bool,
922}
923
924impl Runner<'_> {
925 fn run(&self, q: &Query) -> Result<oqx::OqxResult> {
932 let conn = self.store.conn();
933 let ctx = StoreContext::new(conn, self.repo_id, self.semantic.clone());
934 let plan = if self.planned {
935 SqlitePlanner::new(conn, self.repo_id)
936 .try_plan(q, &[])
937 .map_err(|e| SurfaceError::other(format!("sqlite: {e}")))?
938 } else {
939 None
940 };
941 let (ctx, residual) = match plan {
942 Some(plan) => {
947 let once = !mentions_outside_source(&plan.residual, oqx::ROWS_ROOT);
948 let ctx = if once {
949 ctx.with_rows_root_once(plan.rows)
950 } else {
951 ctx.with_rows_root(plan.rows)
952 };
953 (ctx, Some(plan.residual))
954 }
955 None => (ctx, None),
956 };
957 let engine = InMemoryEngine::new(ctx);
958 let out = engine.run(residual.as_ref().unwrap_or(q), &[]);
959 if let Some(failed) = engine.context().take_root_failure() {
962 return Err(failed.into());
963 }
964 Ok(out?)
967 }
968}
969
970fn run_parsed(
971 engine: &Runner<'_>,
972 raw: &Query,
973 opts: QueryOptions<'_>,
974 mode: UnknownNames,
975) -> Result<OqxResult> {
976 check_root_spellings(raw)?;
977 let own = Query {
985 select: raw
986 .select
987 .iter()
988 .filter(|it| {
989 !RESERVED_ALIAS_PREFIXES
990 .iter()
991 .any(|p| it.name().starts_with(p))
992 })
993 .cloned()
994 .collect(),
995 ..raw.clone()
996 };
997 let local = VocabularyCache::default();
998 let cache = opts.vocabulary.unwrap_or(&local);
999 let hints = check_names(engine.store, engine.repo_id, &own, mode, cache)?;
1000 let mut result = run_resolved(engine, raw, opts)?;
1001 result.hints = hints;
1003 Ok(result)
1004}
1005
1006fn run_resolved(engine: &Runner<'_>, raw: &Query, opts: QueryOptions<'_>) -> Result<OqxResult> {
1007 let parsed = rewrite_query(&resolve_aliases(raw)?);
1010 let consumer = parsed.consumer;
1011
1012 match consumer {
1013 Consumer::Exists => {
1014 let res = engine.run(&parsed)?;
1015 let mut r = OqxResult::scalar(consumer);
1016 r.exists = Some(matches!(res, oqx::OqxResult::Exists(true)));
1017 return Ok(r);
1018 }
1019 Consumer::Count => {
1020 let res = engine.run(&parsed)?;
1021 let mut r = OqxResult::scalar(consumer);
1022 r.count = Some(match res {
1023 oqx::OqxResult::Count(n) => n,
1024 _ => 0.0,
1025 });
1026 return Ok(r);
1027 }
1028 Consumer::None => {
1029 let res = engine.run(&parsed)?;
1030 let mut r = OqxResult::scalar(consumer);
1031 r.none = Some(match res {
1032 oqx::OqxResult::None(b) => b,
1033 _ => true,
1034 });
1035 return Ok(r);
1036 }
1037 Consumer::Collect | Consumer::First | Consumer::Single => {}
1038 }
1039
1040 let top_distinct = parsed.distinct;
1046 let top_values = parsed.values;
1047 let user_select: Vec<SelectItem> = if top_values {
1048 parsed
1049 .select
1050 .first()
1051 .map(|it| match it {
1052 SelectItem::Field {
1053 expr, lift, span, ..
1054 } => SelectItem::Field {
1055 name: VALUE_KEY.to_owned(),
1056 expr: expr.clone(),
1057 lift: *lift,
1058 span: *span,
1059 },
1060 SelectItem::Collect { op, span, .. } => SelectItem::Collect {
1061 name: VALUE_KEY.to_owned(),
1062 op: op.clone(),
1063 span: *span,
1064 },
1065 })
1066 .into_iter()
1067 .collect()
1068 } else {
1069 parsed.select.clone()
1070 };
1071 let bare = opts.bare;
1075 if bare {
1076 check_bare(&parsed)?;
1077 } else if !top_values && let Some(e) = envelope_collision(&user_select) {
1078 return Err(e);
1079 }
1080 let id_item = build::field(ID_KEY, build::ident("id"));
1081 let path_item = build::field(PATH_KEY, build::ident("path"));
1082 let mut select = vec![id_item, path_item];
1083 if !top_values && may_reach_non_rows(&parsed) {
1084 select.push(build::field(SELF_KEY, build::ident("$it")));
1085 }
1086 select.extend(user_select);
1087 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
1091 let q = Query {
1092 distinct: false,
1093 values: false,
1094 select,
1095 limit: if consumer == Consumer::Collect {
1096 None
1097 } else {
1098 parsed.limit.clone()
1099 },
1100 offset: if consumer == Consumer::Collect {
1101 None
1102 } else {
1103 parsed.offset.clone()
1104 },
1105 ..parsed.clone()
1106 };
1107 let res = engine.run(&q)?;
1108
1109 if matches!(consumer, Consumer::First | Consumer::Single) {
1110 let row = match res {
1111 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
1112 _ => None,
1113 };
1114 let mut out = OqxResult::scalar(consumer);
1115 match row {
1116 None => {
1117 if top_values {
1118 out.values = Some(Vec::new());
1119 }
1120 }
1121 Some(r) => {
1122 let shaped = shape_row(r)?;
1123 if top_values {
1124 out.values = Some(vec![value_of(&shaped)]);
1125 } else {
1126 out.hits = vec![render_hit(shaped, bare)];
1127 }
1128 }
1129 }
1130 return Ok(out);
1131 }
1132
1133 let rows: Vec<Value> = match res {
1135 oqx::OqxResult::Collect(rows) => rows,
1136 _ => Vec::new(),
1137 };
1138 let custom = parsed.order_by.as_ref().is_some_and(|o| !o.is_empty());
1139 let cursor = opts.cursor.filter(|c| !c.is_empty() && !custom);
1140 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
1141 let limit = const_bound(top_limit.as_ref(), "limit")?;
1142 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
1143 let (page, truncated, next) = if top_distinct {
1149 let shaped = rows
1150 .into_iter()
1151 .map(shape_row)
1152 .collect::<Result<Vec<_>>>()?;
1153 let shaped = dedup_by_projection(shaped, bare);
1154 page_rows(
1155 shaped,
1156 |s| (s.path.clone(), s.id.clone()),
1157 offset,
1158 limit,
1159 cursor,
1160 cap,
1161 custom,
1162 )?
1163 } else {
1164 let (page, truncated, next) =
1165 page_rows(rows, raw_identity, offset, limit, cursor, cap, custom)?;
1166 let page = page
1167 .into_iter()
1168 .map(shape_row)
1169 .collect::<Result<Vec<_>>>()?;
1170 (page, truncated, next)
1171 };
1172 let mut out = OqxResult::scalar(Consumer::Collect);
1173 out.truncated = truncated;
1174 out.cursor = next;
1175 if top_values {
1176 out.values = Some(page.iter().map(value_of).collect());
1177 } else {
1178 out.hits = page.into_iter().map(|s| render_hit(s, bare)).collect();
1179 }
1180 Ok(out)
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185 use super::*;
1186 use oqx::ast::Where;
1187
1188 #[test]
1189 fn row_functions_become_self_methods() {
1190 let q = oqx::parse_string(
1191 "from blocks where text(\"x\") && under_heading(\"h\") && size(attrs) > 0 && doc.path.startsWith(\"a\")",
1192 )
1193 .unwrap();
1194 let r = rewrite_query(&q);
1195 let Some(Where::And { parts, .. }) = &r.r#where else {
1196 panic!("and")
1197 };
1198 let Where::Scalar { expr, .. } = &parts[0] else {
1199 panic!("scalar")
1200 };
1201 assert!(
1202 matches!(expr, Expr::Call { recv: Some(r), name, .. } if name == "text" && **r == build::ident("$self"))
1203 );
1204 let Where::Scalar { expr, .. } = &parts[2] else {
1205 panic!("scalar")
1206 };
1207 assert!(
1208 matches!(expr, Expr::Binary { left, .. } if matches!(&**left, Expr::Call { recv: None, name, .. } if name == "size"))
1209 );
1210 }
1211
1212 #[test]
1213 fn path_literals_are_rooted_only_against_path_reads() {
1214 let q = oqx::parse_string(
1215 "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\"",
1216 )
1217 .unwrap();
1218 let printed = oqx::print_query(&rewrite_query(&q)).unwrap();
1219 assert!(printed.contains("path.startsWith(\"/lab/\")"), "{printed}");
1220 assert!(printed.contains("doc.path == \"/a.md\""), "{printed}");
1221 assert!(printed.contains("type == \"a.md\""), "{printed}");
1222 assert!(printed.contains("^path == \"/b.md\""), "{printed}");
1223 assert!(printed.contains("\"/x.md\" != dst_path"), "{printed}");
1224 assert!(printed.contains("path == ^after"), "{printed}");
1226 let q = oqx::parse_string(
1228 "from docs where path.contains(\"a/\") && path.endsWith(\".md\") && path == \"\"",
1229 )
1230 .unwrap();
1231 let printed = oqx::print_query(&rewrite_query(&q)).unwrap();
1232 assert!(printed.contains("path.contains(\"a/\")"), "{printed}");
1233 assert!(printed.contains("path.endsWith(\".md\")"), "{printed}");
1234 assert!(printed.contains("path == \"/\""), "{printed}");
1235 }
1236
1237 #[test]
1238 fn repo_and_bare_targets_are_refused_statically() {
1239 let parse = |s: &str| oqx::parse_string(s).unwrap();
1240 for q in [
1241 "from docs where $repo",
1242 "select r: size($repo.docs) from docs",
1243 "select r: $repo.id from docs",
1244 "select r: nodes first { select x: ^$repo.id values } from docs",
1245 "select r: nodes first { select x: 0^$repo values } from docs",
1246 "$repo.docs count { }",
1247 ] {
1248 let e = check_root_spellings(&parse(q)).unwrap_err();
1249 assert_eq!(e.code, "filter_invalid", "{q}");
1250 assert_eq!(e.message, REPO_REMOVED_MESSAGE, "{q}");
1251 }
1252 let e = check_root_spellings(&parse("select x: docs collect { } from docs")).unwrap_err();
1254 assert_eq!(e.message, bare_target_message(Target::Docs, Some(1)));
1255 assert!(
1256 e.message
1257 .contains("did you mean `^docs` (the repository's documents)?")
1258 );
1259 let e = check_root_spellings(&parse(
1260 "select x: nodes collect { select y: size(links) } from docs",
1261 ))
1262 .unwrap_err();
1263 assert!(
1264 e.message
1265 .contains("did you mean `^^links` (the repository's links)?")
1266 );
1267 let e = check_root_spellings(&parse("docs collect { from docs }")).unwrap_err();
1268 assert!(e.message.contains("`^docs`"));
1269 for q in [
1271 "docs count { }",
1272 "from docs where nodes exists { }",
1273 "entries(docs) first { }",
1274 "select n: size(^docs), id: ^id, k: size(0^links) from docs",
1275 "from blocks where blocks exists { }", ] {
1277 check_root_spellings(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1278 }
1279 let e = check_root_spellings(&parse("^docs count { }")).unwrap_err();
1281 assert_eq!(e.message, past_root_message(Target::Docs, 1));
1282 assert_eq!(
1283 e.message,
1284 "`^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)`)"
1285 );
1286 let e = check_root_spellings(&parse("select path from docs where ^^links exists { }"))
1287 .unwrap_err();
1288 assert!(
1289 e.message.contains("`^^links` reaches past the root"),
1290 "{}",
1291 e.message
1292 );
1293 let m = bare_target_message(Target::Blocks, None);
1295 assert!(m.contains("did you mean `^blocks` (the repository's blocks; one caret per enclosing block, or the absolute `0^blocks`)?"), "{m}");
1296 }
1297
1298 #[test]
1299 fn reserved_projection_names_are_refused() {
1300 let parse = |s: &str| oqx::parse_string(s).unwrap();
1301 let e = check_reserved_aliases(&parse("select __oqx_id: 1 from docs")).unwrap_err();
1302 assert_eq!(e.code, "filter_invalid");
1303 assert_eq!(
1304 e.message,
1305 "projection `__oqx_id` uses the reserved prefix `__oqx_` — choose another alias"
1306 );
1307 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1308 let e = check_reserved_aliases(&parse("select __omg_sel_0_x: 1 from docs")).unwrap_err();
1309 assert!(
1310 e.message.contains("reserved prefix `__omg_`"),
1311 "{}",
1312 e.message
1313 );
1314 assert!(
1316 check_reserved_aliases(&parse(
1317 "select __oqx: 1, x: nodes collect { select __oqx_y: 1 } from docs"
1318 ))
1319 .is_ok()
1320 );
1321 }
1322
1323 #[test]
1324 fn semantic_phrases_are_collected_distinct() {
1325 let phrases = collect_semantic_phrases(
1326 "select s: semantic(\"alpha\") from docs where semantic(\"alpha\") > 0.5 || nodes exists { where semantic(\"beta\") > 0 } order by semantic(\"gamma\") desc",
1327 );
1328 assert_eq!(phrases, ["alpha", "beta", "gamma"]);
1329 assert!(collect_semantic_phrases("not a query {{").is_empty());
1330 assert!(collect_semantic_phrases("from docs").is_empty());
1331 }
1332
1333 #[test]
1334 fn hits_peel_the_injected_columns() {
1335 let mut o = oqx::Object::new();
1336 o.insert(ID_KEY, Value::Str("d_1".into()));
1337 o.insert(PATH_KEY, Value::Null);
1338 o.insert("layer", Value::Str("canon".into()));
1339 let shaped = shape_row(Value::Object(o)).unwrap();
1340 assert_eq!((shaped.id.as_str(), shaped.path.as_str()), ("d_1", ""));
1341 let hit = to_hit(shaped);
1342 let ho = hit.as_object().unwrap();
1343 assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path", "layer"]);
1344 assert_eq!(ho.get("path"), Some(&Value::Str(String::new())));
1345 let mut o = oqx::Object::new();
1348 o.insert(ID_KEY, Value::Str("d_1".into()));
1349 o.insert(PATH_KEY, Value::Str("a.md".into()));
1350 o.insert("id", Value::Number(7.0));
1351 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1352 let ho = hit.as_object().unwrap();
1353 assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path"]);
1354 assert_eq!(ho.get("id"), Some(&Value::Number(7.0)));
1355 let mut o = oqx::Object::new();
1357 o.insert(ID_KEY, Value::Str("d_1".into()));
1358 o.insert(PATH_KEY, Value::Undefined);
1359 let row = Value::Object(o);
1360 assert_eq!(raw_identity(&row), (String::new(), "d_1".to_owned()));
1361 let shaped = shape_row(row).unwrap();
1362 assert_eq!((shaped.path, shaped.id), (String::new(), "d_1".to_owned()));
1363 }
1364
1365 #[test]
1366 fn bare_hits_are_the_projection_in_its_own_order() {
1367 let mut o = oqx::Object::new();
1370 o.insert(ID_KEY, Value::Str("d_1".into()));
1371 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1372 o.insert("title", Value::Str("T".into()));
1373 o.insert("id", Value::Str("d_1".into()));
1374 let shaped = shape_row(Value::Object(o)).unwrap();
1375 let bare = to_bare_hit(shaped);
1376 let bo = bare.as_object().unwrap();
1377 assert_eq!(bo.keys().collect::<Vec<_>>(), ["title", "id"]);
1378 let mut o = oqx::Object::new();
1381 o.insert(ID_KEY, Value::Str("d_1".into()));
1382 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1383 o.insert("path", Value::Str("canon".into()));
1384 let shaped = shape_row(Value::Object(o)).unwrap();
1385 assert_eq!(shaped.path, "/a.md");
1386 assert_eq!(
1387 render_hit(shaped, true),
1388 serde_json::json!({ "path": "canon" })
1389 );
1390 let mut o = oqx::Object::new();
1392 o.insert(ID_KEY, Value::Str("d_1".into()));
1393 o.insert(PATH_KEY, Value::Str("/a.md".into()));
1394 o.insert("path", Value::Str("/a.md".into()));
1395 assert_eq!(
1396 render_hit(shape_row(Value::Object(o)).unwrap(), false),
1397 serde_json::json!({ "id": "d_1", "path": "/a.md" })
1398 );
1399 }
1400
1401 #[test]
1402 fn bare_needs_a_select_and_is_not_the_values_form() {
1403 let parse = |s: &str| oqx::parse_string(s).unwrap();
1404 for q in ["from docs", "from docs where layer == \"canon\""] {
1405 let e = check_bare(&parse(q)).unwrap_err();
1406 assert_eq!(e.code, "filter_invalid", "{q}");
1407 assert_eq!(e.message, BARE_NEEDS_SELECT_MESSAGE, "{q}");
1408 assert_eq!(e.data.as_ref().unwrap()["hint"], "OQX");
1409 }
1410 let e = check_bare(&parse("select path values from docs")).unwrap_err();
1411 assert_eq!(e.code, "filter_invalid");
1412 assert_eq!(e.message, BARE_WITH_VALUES_MESSAGE);
1413 for q in [
1414 "select path from docs",
1415 "select id, title from docs",
1416 "docs first { select t: title }",
1417 ] {
1418 check_bare(&parse(q)).unwrap_or_else(|e| panic!("{q}: {e}"));
1419 }
1420 }
1421
1422 #[test]
1423 fn a_projection_onto_the_envelope_must_be_aliased() {
1424 let items = |src: &str| oqx::parse_string(src).unwrap().select;
1425 assert!(envelope_collision(&items("select id, path from docs")).is_none());
1427 assert!(
1428 envelope_collision(&items("select id: id, path: path, p: path from docs")).is_none()
1429 );
1430 let e = envelope_collision(&items("select data.path from docs")).unwrap();
1432 assert_eq!(e.code, "filter_invalid");
1433 assert_eq!(
1434 e.message,
1435 "projection `path` collides with the hit envelope — alias it (`p: data.path`)"
1436 );
1437 let e = envelope_collision(&items("select id: layer from docs")).unwrap();
1438 assert_eq!(
1439 e.message,
1440 "projection `id` collides with the hit envelope — alias it (`i: data.id`)"
1441 );
1442 assert!(envelope_collision(&items("select path: doc.path from blocks")).is_some());
1443 assert!(envelope_collision(&items("select path: nodes collect { } from docs")).is_some());
1444 }
1445
1446 #[test]
1447 fn a_path_read_is_the_attribute_or_a_row_handle_never_a_source_handle() {
1448 let pred = |src: &str| match oqx::parse_string(src).unwrap().r#where.unwrap() {
1449 oqx::ast::Where::Scalar { expr, .. } => expr,
1450 other => panic!("{other:?}"),
1451 };
1452 let left = |e: Expr| match e {
1453 Expr::Binary { left, .. } => *left,
1454 other => panic!("{other:?}"),
1455 };
1456 assert!(is_path_read(&left(pred("from docs where path == \"x\""))));
1457 assert!(is_path_read(&left(pred(
1458 "from blocks where doc.path == \"x\""
1459 ))));
1460 assert!(is_path_read(&left(pred("from docs where ^path == \"x\""))));
1461 assert!(is_path_read(&left(pred(
1462 "from links where dst_path == \"x\""
1463 ))));
1464 assert!(!is_path_read(&left(pred(
1465 "from docs where data.path == \"x\""
1466 ))));
1467 assert!(!is_path_read(&left(pred(
1468 "from docs where frontmatter.path == \"x\""
1469 ))));
1470 }
1471
1472 #[test]
1473 fn a_hit_that_is_not_a_store_row_fails_the_query() {
1474 let mut o = oqx::Object::new();
1477 o.insert(ID_KEY, Value::Undefined);
1478 o.insert(PATH_KEY, Value::Undefined);
1479 o.insert(SELF_KEY, Value::Str("/timeline/kickoff.md".into()));
1480 let e = shape_row(Value::Object(o)).unwrap_err();
1481 assert_eq!(e.code, "filter_invalid");
1482 assert_eq!(
1483 e.message,
1484 "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>)"
1485 );
1486 let mut row = oqx::Object::new();
1488 row.insert("doc_id", Value::Str("d_1".into()));
1489 row.insert("path", Value::Str("a.md".into()));
1490 let row = crate::context::tag_row(row, crate::context::Target::Docs);
1491 let mut o = oqx::Object::new();
1492 o.insert(ID_KEY, Value::Str("d_1".into()));
1493 o.insert(PATH_KEY, Value::Str("a.md".into()));
1494 o.insert(SELF_KEY, row);
1495 let hit = to_hit(shape_row(Value::Object(o)).unwrap());
1496 assert_eq!(
1497 hit.as_object().unwrap().keys().collect::<Vec<_>>(),
1498 ["id", "path"]
1499 );
1500 let parse = |s: &str| oqx::parse_string(s).unwrap();
1502 assert!(!may_reach_non_rows(&parse(
1503 "from docs where layer == \"canon\""
1504 )));
1505 assert!(may_reach_non_rows(&parse("from docs follow before")));
1506 assert!(may_reach_non_rows(&parse("refs(\"/index.md\") first { }")));
1507 }
1508
1509 #[test]
1510 fn distinct_dedups_by_user_projection_first_wins() {
1511 let mk = |id: &str, t: &str| {
1512 let mut proj = oqx::Object::new();
1513 proj.insert("type", Value::Str(t.into()));
1514 Shaped {
1515 id: id.to_owned(),
1516 path: "p".to_owned(),
1517 proj,
1518 }
1519 };
1520 let out = dedup_by_projection(vec![mk("1", "a"), mk("2", "b"), mk("3", "a")], false);
1521 assert_eq!(out.len(), 2);
1522 assert_eq!(out[0].id, "1");
1523 assert_eq!(out[1].id, "2");
1524 let with_id = |id: &str| {
1528 let mut proj = oqx::Object::new();
1529 proj.insert("id", Value::Str(id.into()));
1530 Shaped {
1531 id: id.to_owned(),
1532 path: "p".to_owned(),
1533 proj,
1534 }
1535 };
1536 assert_eq!(
1537 dedup_by_projection(vec![with_id("1"), with_id("2")], false).len(),
1538 1
1539 );
1540 assert_eq!(
1541 dedup_by_projection(vec![with_id("1"), with_id("2")], true).len(),
1542 2
1543 );
1544 }
1545
1546 #[test]
1547 fn the_page_runs_on_identity() {
1548 let rows: Vec<(String, String)> = [("/a.md", "d_1"), ("/a.md", "d_2"), ("/b.md", "d_3")]
1549 .iter()
1550 .map(|(p, i)| ((*p).to_owned(), (*i).to_owned()))
1551 .collect();
1552 let ident = |r: &(String, String)| (r.0.clone(), r.1.clone());
1553 let (page, truncated, next) =
1555 page_rows(rows.clone(), ident, 0, None, None, 2, false).unwrap();
1556 assert_eq!(page.len(), 2);
1557 assert!(truncated);
1558 let next = next.expect("a cursor");
1559 assert_eq!(next, encode_cursor(&["/a.md", "d_2"]));
1560 let (page, truncated, next) =
1562 page_rows(rows.clone(), ident, 0, None, Some(&next), 2, false).unwrap();
1563 assert_eq!(page, vec![("/b.md".to_owned(), "d_3".to_owned())]);
1564 assert!(!truncated && next.is_none());
1565 let (page, truncated, next) =
1567 page_rows(rows.clone(), ident, 1, Some(1), None, 50, true).unwrap();
1568 assert_eq!(page, vec![("/a.md".to_owned(), "d_2".to_owned())]);
1569 assert!(!truncated && next.is_none());
1570 let (page, truncated, next) = page_rows(rows, ident, 0, None, None, 1, true).unwrap();
1571 assert_eq!(page.len(), 1);
1572 assert!(truncated && next.is_none());
1573 }
1574
1575 #[test]
1576 fn top_level_bounds_must_be_literals() {
1577 assert_eq!(const_bound(None, "limit").unwrap(), None);
1578 assert_eq!(
1579 const_bound(
1580 Some(&Expr::Lit {
1581 value: Value::Number(3.0),
1582 span: oqx::Span::EMPTY
1583 }),
1584 "limit"
1585 )
1586 .unwrap(),
1587 Some(3)
1588 );
1589 let e = const_bound(
1590 Some(&Expr::Lit {
1591 value: Value::Number(-1.0),
1592 span: oqx::Span::EMPTY,
1593 }),
1594 "offset",
1595 )
1596 .unwrap_err();
1597 assert_eq!(e.code, "filter_invalid");
1598 assert!(e.message.contains("top-level offset"));
1599 }
1600}