1use std::collections::HashMap;
11
12use omgbase_search::{EmbeddingProvider, f32_to_blob};
13use omgbase_store::Store;
14use oqx::ast::{Expr, Follow, OpNode, OrderSpec, Query, SelectItem, Subquery, Where};
15use oqx::{Consumer, Engine, InMemoryEngine, Value};
16use serde_json::{Map, Value as Json};
17
18use crate::context::{SemanticVec, StoreContext, strip_tags};
19use crate::cursor::{decode_cursor, encode_cursor};
20use crate::error::{Result, SurfaceError};
21
22pub const DEFAULT_LIMIT: usize = 50;
24
25const ROW_FNS: [&str; 12] = [
28 "text",
29 "semantic",
30 "under",
31 "under_heading",
32 "within",
33 "under_kind",
34 "yaml_path",
35 "json_pointer",
36 "has_edge",
37 "has_anchor",
38 "child_count",
39 "parent_type",
40];
41
42const ID_KEY: &str = "__oqx_id";
43const PATH_KEY: &str = "__oqx_path";
44const VALUE_KEY: &str = "__oqx_value";
48
49#[derive(Clone, Copy, Default)]
51pub struct QueryOptions<'a> {
52 pub limit: Option<usize>,
54 pub cursor: Option<&'a str>,
56 pub provider: Option<&'a dyn EmbeddingProvider>,
59}
60
61#[derive(Clone, Debug, PartialEq)]
63pub struct OqxResult {
64 pub hits: Vec<Json>,
65 pub truncated: bool,
66 pub cursor: Option<String>,
67 pub consumer: Consumer,
68 pub count: Option<f64>,
69 pub exists: Option<bool>,
70 pub none: Option<bool>,
71 pub values: Option<Vec<Json>>,
73}
74
75impl OqxResult {
76 fn scalar(consumer: Consumer) -> Self {
77 Self {
78 hits: Vec::new(),
79 truncated: false,
80 cursor: None,
81 consumer,
82 count: None,
83 exists: None,
84 none: None,
85 values: None,
86 }
87 }
88
89 #[must_use]
92 pub fn to_json(&self) -> Json {
93 let mut m = Map::new();
94 m.insert("hits".to_owned(), Json::Array(self.hits.clone()));
95 m.insert("truncated".to_owned(), Json::Bool(self.truncated));
96 m.insert(
97 "cursor".to_owned(),
98 self.cursor.clone().map_or(Json::Null, Json::String),
99 );
100 m.insert(
101 "consumer".to_owned(),
102 Json::String(self.consumer.as_str().to_owned()),
103 );
104 if let Some(n) = self.count {
105 m.insert("count".to_owned(), Value::Number(n).to_canonical_json());
106 }
107 if let Some(b) = self.exists {
108 m.insert("exists".to_owned(), Json::Bool(b));
109 }
110 if let Some(b) = self.none {
111 m.insert("none".to_owned(), Json::Bool(b));
112 }
113 if let Some(v) = &self.values {
114 m.insert("values".to_owned(), Json::Array(v.clone()));
115 }
116 Json::Object(m)
117 }
118}
119
120fn self_ref() -> Expr {
123 Expr::Ident {
124 name: "$self".to_owned(),
125 }
126}
127
128fn rewrite_expr(e: &Expr) -> Expr {
129 match e {
130 Expr::Member { recv, name } => Expr::Member {
131 recv: Box::new(rewrite_expr(recv)),
132 name: name.clone(),
133 },
134 Expr::Index { recv, index } => Expr::Index {
135 recv: Box::new(rewrite_expr(recv)),
136 index: Box::new(rewrite_expr(index)),
137 },
138 Expr::Unary { op, expr } => Expr::Unary {
139 op: *op,
140 expr: Box::new(rewrite_expr(expr)),
141 },
142 Expr::Binary { op, left, right } => Expr::Binary {
143 op: *op,
144 left: Box::new(rewrite_expr(left)),
145 right: Box::new(rewrite_expr(right)),
146 },
147 Expr::Logical { op, left, right } => Expr::Logical {
148 op: *op,
149 left: Box::new(rewrite_expr(left)),
150 right: Box::new(rewrite_expr(right)),
151 },
152 Expr::In { left, right } => Expr::In {
153 left: Box::new(rewrite_expr(left)),
154 right: Box::new(rewrite_expr(right)),
155 },
156 Expr::Range {
157 lo,
158 hi,
159 exclusive_end,
160 } => Expr::Range {
161 lo: lo.as_ref().map(|x| Box::new(rewrite_expr(x))),
162 hi: hi.as_ref().map(|x| Box::new(rewrite_expr(x))),
163 exclusive_end: *exclusive_end,
164 },
165 Expr::Call { recv, name, args } => {
166 let args = args.iter().map(rewrite_expr).collect();
167 match recv {
168 None if ROW_FNS.contains(&name.as_str()) => Expr::Call {
169 recv: Some(Box::new(self_ref())),
170 name: name.clone(),
171 args,
172 },
173 None => Expr::Call {
174 recv: None,
175 name: name.clone(),
176 args,
177 },
178 Some(r) => Expr::Call {
179 recv: Some(Box::new(rewrite_expr(r))),
180 name: name.clone(),
181 args,
182 },
183 }
184 }
185 Expr::Lit(_) | Expr::Ident { .. } | Expr::Outer { .. } | Expr::Binding { .. } => e.clone(),
186 }
187}
188
189fn rewrite_where(w: &Where) -> Where {
190 match w {
191 Where::And { parts } => Where::And {
192 parts: parts.iter().map(rewrite_where).collect(),
193 },
194 Where::Or { parts } => Where::Or {
195 parts: parts.iter().map(rewrite_where).collect(),
196 },
197 Where::Not { expr } => Where::Not {
198 expr: Box::new(rewrite_where(expr)),
199 },
200 Where::Scalar { expr } => Where::Scalar {
201 expr: rewrite_expr(expr),
202 },
203 Where::Op(op) => Where::Op(Box::new(rewrite_op(op))),
204 }
205}
206
207fn rewrite_op(op: &OpNode) -> OpNode {
208 OpNode {
209 receiver: rewrite_expr(&op.receiver),
210 op: op.op,
211 sub: rewrite_sub(&op.sub),
212 count_cmp: op.count_cmp.clone(),
213 distinct: op.distinct,
214 }
215}
216
217fn rewrite_follow(f: &Follow) -> Follow {
218 Follow {
219 receiver: rewrite_expr(&f.receiver),
220 distinct: f.distinct,
221 r#where: f.r#where.as_ref().map(rewrite_expr),
222 frontier: f.frontier.as_ref().map(rewrite_expr),
223 depth: f.depth,
224 by: f.by.as_ref().map(rewrite_expr),
225 }
226}
227
228fn rewrite_select(items: &[SelectItem]) -> Vec<SelectItem> {
229 items
230 .iter()
231 .map(|it| match it {
232 SelectItem::Field { name, expr, lift } => SelectItem::Field {
233 name: name.clone(),
234 expr: rewrite_expr(expr),
235 lift: *lift,
236 },
237 SelectItem::Collect { name, op } => SelectItem::Collect {
238 name: name.clone(),
239 op: Box::new(rewrite_op(op)),
240 },
241 })
242 .collect()
243}
244
245fn rewrite_order(o: Option<&Vec<OrderSpec>>) -> Option<Vec<OrderSpec>> {
246 o.map(|specs| {
247 specs
248 .iter()
249 .map(|s| OrderSpec {
250 expr: rewrite_expr(&s.expr),
251 desc: s.desc,
252 })
253 .collect()
254 })
255}
256
257fn rewrite_sub(s: &Subquery) -> Subquery {
258 Subquery {
259 from: s.from.iter().map(rewrite_expr).collect(),
260 r#where: s.r#where.as_ref().map(rewrite_where),
261 select: rewrite_select(&s.select),
262 order_by: rewrite_order(s.order_by.as_ref()),
263 follow: s.follow.as_ref().map(rewrite_follow),
264 values: s.values,
265 limit: s.limit.as_ref().map(rewrite_expr),
266 offset: s.offset.as_ref().map(rewrite_expr),
267 }
268}
269
270#[must_use]
272pub fn rewrite_query(q: &Query) -> Query {
273 Query {
274 source: rewrite_expr(&q.source),
275 from: q.from.iter().map(rewrite_expr).collect(),
276 r#where: q.r#where.as_ref().map(rewrite_where),
277 select: rewrite_select(&q.select),
278 order_by: rewrite_order(q.order_by.as_ref()),
279 consumer: q.consumer,
280 follow: q.follow.as_ref().map(rewrite_follow),
281 distinct: q.distinct,
282 values: q.values,
283 limit: q.limit.as_ref().map(rewrite_expr),
284 offset: q.offset.as_ref().map(rewrite_expr),
285 }
286}
287
288fn visit_expr(e: &Expr, out: &mut Vec<String>) {
291 match e {
292 Expr::Call { recv, name, args } => {
293 if recv.is_none() && name == "semantic" {
294 if let Some(Expr::Lit(Value::Str(s))) = args.first() {
295 if !out.contains(s) {
296 out.push(s.clone());
297 }
298 }
299 }
300 if let Some(r) = recv {
301 visit_expr(r, out);
302 }
303 for a in args {
304 visit_expr(a, out);
305 }
306 }
307 Expr::Member { recv, .. } => visit_expr(recv, out),
308 Expr::Index { recv, index } => {
309 visit_expr(recv, out);
310 visit_expr(index, out);
311 }
312 Expr::Unary { expr, .. } => visit_expr(expr, out),
313 Expr::Binary { left, right, .. }
314 | Expr::Logical { left, right, .. }
315 | Expr::In { left, right } => {
316 visit_expr(left, out);
317 visit_expr(right, out);
318 }
319 Expr::Range { lo, hi, .. } => {
320 if let Some(l) = lo {
321 visit_expr(l, out);
322 }
323 if let Some(h) = hi {
324 visit_expr(h, out);
325 }
326 }
327 Expr::Lit(_) | Expr::Ident { .. } | Expr::Outer { .. } | Expr::Binding { .. } => {}
328 }
329}
330
331fn visit_where(w: &Where, out: &mut Vec<String>) {
332 match w {
333 Where::And { parts } | Where::Or { parts } => {
334 parts.iter().for_each(|p| visit_where(p, out))
335 }
336 Where::Not { expr } => visit_where(expr, out),
337 Where::Scalar { expr } => visit_expr(expr, out),
338 Where::Op(op) => visit_op(op, out),
339 }
340}
341
342fn visit_op(op: &OpNode, out: &mut Vec<String>) {
343 visit_expr(&op.receiver, out);
344 visit_sub(&op.sub, out);
345}
346
347fn visit_select(items: &[SelectItem], out: &mut Vec<String>) {
348 for it in items {
349 match it {
350 SelectItem::Field { expr, .. } => visit_expr(expr, out),
351 SelectItem::Collect { op, .. } => visit_op(op, out),
352 }
353 }
354}
355
356fn visit_follow(f: &Follow, out: &mut Vec<String>) {
357 visit_expr(&f.receiver, out);
358 for x in [&f.r#where, &f.frontier, &f.by].into_iter().flatten() {
359 visit_expr(x, out);
360 }
361}
362
363fn visit_sub(s: &Subquery, out: &mut Vec<String>) {
364 s.from.iter().for_each(|e| visit_expr(e, out));
365 if let Some(w) = &s.r#where {
366 visit_where(w, out);
367 }
368 visit_select(&s.select, out);
369 if let Some(o) = &s.order_by {
370 o.iter().for_each(|spec| visit_expr(&spec.expr, out));
371 }
372 if let Some(f) = &s.follow {
373 visit_follow(f, out);
374 }
375}
376
377#[must_use]
380pub fn collect_semantic_phrases(source: &str) -> Vec<String> {
381 let Ok(q) = oqx::parse_string(source) else {
382 return Vec::new();
383 };
384 let mut out = Vec::new();
385 visit_expr(&q.source, &mut out);
386 q.from.iter().for_each(|e| visit_expr(e, &mut out));
387 if let Some(w) = &q.r#where {
388 visit_where(w, &mut out);
389 }
390 visit_select(&q.select, &mut out);
391 if let Some(o) = &q.order_by {
392 o.iter().for_each(|spec| visit_expr(&spec.expr, &mut out));
393 }
394 if let Some(f) = &q.follow {
395 visit_follow(f, &mut out);
396 }
397 out
398}
399
400fn to_hit(row: Value) -> Value {
406 let Value::Object(o) = strip_tags(row) else {
407 return Value::Object(oqx::Object::new());
408 };
409 let mut id = Value::Undefined;
410 let mut path = Value::Undefined;
411 let mut rest = Vec::new();
412 for (k, v) in o {
413 match k.as_str() {
414 ID_KEY => id = v,
415 PATH_KEY => path = v,
416 _ => rest.push((k, v)),
417 }
418 }
419 let mut hit = oqx::Object::with_capacity(rest.len() + 2);
420 hit.insert("id", Value::Str(id.to_string()));
421 hit.insert(
422 "path",
423 Value::Str(if path.is_absent() {
424 String::new()
425 } else {
426 path.to_string()
427 }),
428 );
429 for (k, v) in rest {
430 hit.insert(k, v);
431 }
432 Value::Object(hit)
433}
434
435fn hit_str(hit: &Value, key: &str) -> String {
436 hit.as_object()
437 .and_then(|o| o.get(key))
438 .map(|v| v.to_string())
439 .unwrap_or_default()
440}
441
442fn dedup_hits_by_projection(hits: Vec<Value>) -> Vec<Value> {
446 let mut seen: Vec<String> = Vec::new();
447 let mut out = Vec::new();
448 for h in hits {
449 let mut pairs: Vec<(String, Value)> = h
450 .as_object()
451 .map(|o| {
452 o.iter()
453 .filter(|(k, _)| *k != "id" && *k != "path")
454 .map(|(k, v)| (k.to_owned(), v.clone()))
455 .collect()
456 })
457 .unwrap_or_default();
458 pairs.sort_by(|a, b| a.0.cmp(&b.0));
459 let key = Value::Array(
460 pairs
461 .into_iter()
462 .map(|(k, v)| Value::Array(vec![Value::Str(k), v]))
463 .collect(),
464 )
465 .to_canonical_json()
466 .to_string();
467 if seen.contains(&key) {
468 continue;
469 }
470 seen.push(key);
471 out.push(h);
472 }
473 out
474}
475
476fn const_bound(e: Option<&Expr>, word: &str) -> Result<Option<usize>> {
479 match e {
480 None => Ok(None),
481 Some(Expr::Lit(Value::Number(n))) if n.fract() == 0.0 && *n >= 0.0 && n.is_finite() => {
482 Ok(Some(*n as usize))
483 }
484 Some(_) => Err(SurfaceError::filter_invalid(
485 format!("top-level {word} must be a non-negative integer literal"),
486 "OQX",
487 )),
488 }
489}
490
491fn value_of(hit: &Value) -> Value {
492 hit.as_object()
493 .and_then(|o| o.get(VALUE_KEY))
494 .cloned()
495 .unwrap_or(Value::Undefined)
496}
497
498fn without_value_key(hit: Value) -> Json {
499 strip_tags(hit).to_canonical_json()
500}
501
502pub fn query(
506 store: &Store,
507 repo_id: &str,
508 source: &str,
509 opts: QueryOptions<'_>,
510) -> Result<OqxResult> {
511 let phrases = collect_semantic_phrases(source);
516 let mut semantic: HashMap<String, SemanticVec> = HashMap::new();
517 if let Some(provider) = opts.provider.filter(|_| !phrases.is_empty()) {
518 for phrase in phrases {
519 let vec = provider
520 .embed_query(&phrase)
521 .map_err(|e| SurfaceError::new(e.code(), e.to_string()))?;
522 semantic.insert(
523 phrase,
524 SemanticVec {
525 model: provider.model().to_owned(),
526 vec: f32_to_blob(&vec),
527 },
528 );
529 }
530 }
531 let engine = InMemoryEngine::new(StoreContext::new(store.conn(), repo_id, semantic));
532 let out = run_inner(&engine, source, opts);
533 if let Some(pending) = engine.context().take_pending() {
537 return Err(pending);
538 }
539 out
540}
541
542fn run_inner(
543 engine: &InMemoryEngine<StoreContext<'_>>,
544 source: &str,
545 opts: QueryOptions<'_>,
546) -> Result<OqxResult> {
547 let parsed = rewrite_query(&oqx::parse_string(source)?);
548 let consumer = parsed.consumer;
549
550 match consumer {
551 Consumer::Exists => {
552 let res = engine.run(&parsed, &[])?;
553 let mut r = OqxResult::scalar(consumer);
554 r.exists = Some(matches!(res, oqx::OqxResult::Exists(true)));
555 return Ok(r);
556 }
557 Consumer::Count => {
558 let res = engine.run(&parsed, &[])?;
559 let mut r = OqxResult::scalar(consumer);
560 r.count = Some(match res {
561 oqx::OqxResult::Count(n) => n,
562 _ => 0.0,
563 });
564 return Ok(r);
565 }
566 Consumer::None => {
567 let res = engine.run(&parsed, &[])?;
568 let mut r = OqxResult::scalar(consumer);
569 r.none = Some(match res {
570 oqx::OqxResult::None(b) => b,
571 _ => true,
572 });
573 return Ok(r);
574 }
575 Consumer::Collect | Consumer::First | Consumer::Single => {}
576 }
577
578 let top_distinct = parsed.distinct;
584 let top_values = parsed.values;
585 let user_select: Vec<SelectItem> = if top_values {
586 parsed
587 .select
588 .first()
589 .map(|it| match it {
590 SelectItem::Field { expr, lift, .. } => SelectItem::Field {
591 name: VALUE_KEY.to_owned(),
592 expr: expr.clone(),
593 lift: *lift,
594 },
595 SelectItem::Collect { op, .. } => SelectItem::Collect {
596 name: VALUE_KEY.to_owned(),
597 op: op.clone(),
598 },
599 })
600 .into_iter()
601 .collect()
602 } else {
603 parsed.select.clone()
604 };
605 let id_item = SelectItem::Field {
606 name: ID_KEY.to_owned(),
607 expr: Expr::Ident {
608 name: "$id".to_owned(),
609 },
610 lift: 0,
611 };
612 let path_item = SelectItem::Field {
613 name: PATH_KEY.to_owned(),
614 expr: Expr::Ident {
615 name: "$path".to_owned(),
616 },
617 lift: 0,
618 };
619 let mut select = vec![id_item, path_item];
620 select.extend(user_select);
621 let (top_limit, top_offset) = (parsed.limit.clone(), parsed.offset.clone());
625 let q = Query {
626 distinct: false,
627 values: false,
628 select,
629 limit: if consumer == Consumer::Collect {
630 None
631 } else {
632 parsed.limit.clone()
633 },
634 offset: if consumer == Consumer::Collect {
635 None
636 } else {
637 parsed.offset.clone()
638 },
639 ..parsed.clone()
640 };
641 let res = engine.run(&q, &[])?;
642
643 if matches!(consumer, Consumer::First | Consumer::Single) {
644 let row = match res {
645 oqx::OqxResult::First(r) | oqx::OqxResult::Single(r) => r,
646 _ => None,
647 };
648 let mut out = OqxResult::scalar(consumer);
649 match row {
650 None => {
651 if top_values {
652 out.values = Some(Vec::new());
653 }
654 }
655 Some(r) => {
656 let hit = to_hit(r);
657 if top_values {
658 out.values = Some(vec![strip_tags(value_of(&hit)).to_canonical_json()]);
659 } else {
660 out.hits = vec![without_value_key(hit)];
661 }
662 }
663 }
664 return Ok(out);
665 }
666
667 let mut rows: Vec<Value> = match res {
669 oqx::OqxResult::Collect(rows) => rows.into_iter().map(to_hit).collect(),
670 _ => Vec::new(),
671 };
672 if top_distinct {
673 rows = dedup_hits_by_projection(rows);
674 }
675 let offset = const_bound(top_offset.as_ref(), "offset")?.unwrap_or(0);
676 let limit = const_bound(top_limit.as_ref(), "limit")?;
677 if offset > 0 || limit.is_some() {
678 let end = limit.map_or(rows.len(), |l| (offset + l).min(rows.len()));
679 rows = if offset >= rows.len() {
680 Vec::new()
681 } else {
682 rows[offset..end].to_vec()
683 };
684 }
685 let custom = parsed.order_by.as_ref().is_some_and(|o| !o.is_empty());
686 let cap = opts.limit.unwrap_or(DEFAULT_LIMIT);
687 let mut page = rows;
688 if !custom {
689 if let Some(cursor) = opts.cursor.filter(|c| !c.is_empty()) {
690 let parts = decode_cursor(cursor, "query", 2)?;
691 let (path, id) = (&parts[0], &parts[1]);
692 page.retain(|h| {
693 let hp = hit_str(h, "path");
694 let hi = hit_str(h, "id");
695 hp > *path || (hp == *path && hi > *id)
696 });
697 }
698 }
699 let truncated = page.len() > cap;
700 page.truncate(cap);
701 let cursor = if truncated && !custom {
702 page.last()
703 .map(|last| encode_cursor(&[&hit_str(last, "path"), &hit_str(last, "id")]))
704 } else {
705 None
706 };
707 let mut out = OqxResult::scalar(Consumer::Collect);
708 out.truncated = truncated;
709 out.cursor = cursor;
710 if top_values {
711 out.values = Some(
712 page.iter()
713 .map(|h| strip_tags(value_of(h)).to_canonical_json())
714 .collect(),
715 );
716 } else {
717 out.hits = page.into_iter().map(without_value_key).collect();
718 }
719 Ok(out)
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 #[test]
727 fn row_functions_become_self_methods() {
728 let q = oqx::parse_string(
729 "from blocks where text(\"x\") && under_heading(\"h\") && size(attrs) > 0 && doc.$path.startsWith(\"a\")",
730 )
731 .unwrap();
732 let r = rewrite_query(&q);
733 let Some(Where::And { parts }) = &r.r#where else {
734 panic!("and")
735 };
736 let Where::Scalar { expr } = &parts[0] else {
737 panic!("scalar")
738 };
739 assert!(
740 matches!(expr, Expr::Call { recv: Some(r), name, .. } if name == "text" && **r == self_ref())
741 );
742 let Where::Scalar { expr } = &parts[2] else {
743 panic!("scalar")
744 };
745 assert!(
746 matches!(expr, Expr::Binary { left, .. } if matches!(&**left, Expr::Call { recv: None, name, .. } if name == "size"))
747 );
748 }
749
750 #[test]
751 fn semantic_phrases_are_collected_distinct() {
752 let phrases = collect_semantic_phrases(
753 "select s: semantic(\"alpha\") from docs where semantic(\"alpha\") > 0.5 || nodes exists { where semantic(\"beta\") > 0 } order by semantic(\"gamma\") desc",
754 );
755 assert_eq!(phrases, ["alpha", "beta", "gamma"]);
756 assert!(collect_semantic_phrases("not a query {{").is_empty());
757 assert!(collect_semantic_phrases("from docs").is_empty());
758 }
759
760 #[test]
761 fn hits_peel_the_injected_columns() {
762 let mut o = oqx::Object::new();
763 o.insert(ID_KEY, Value::Str("d_1".into()));
764 o.insert(PATH_KEY, Value::Null);
765 o.insert("layer", Value::Str("canon".into()));
766 let hit = to_hit(Value::Object(o));
767 let ho = hit.as_object().unwrap();
768 assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path", "layer"]);
769 assert_eq!(ho.get("path"), Some(&Value::Str(String::new())));
770 let mut o = oqx::Object::new();
772 o.insert(ID_KEY, Value::Str("d_1".into()));
773 o.insert(PATH_KEY, Value::Str("a.md".into()));
774 o.insert("id", Value::Number(7.0));
775 let hit = to_hit(Value::Object(o));
776 let ho = hit.as_object().unwrap();
777 assert_eq!(ho.keys().collect::<Vec<_>>(), ["id", "path"]);
778 assert_eq!(ho.get("id"), Some(&Value::Number(7.0)));
779 }
780
781 #[test]
782 fn distinct_dedups_by_user_projection_first_wins() {
783 let mk = |id: &str, t: &str| {
784 let mut o = oqx::Object::new();
785 o.insert("id", Value::Str(id.into()));
786 o.insert("path", Value::Str("p".into()));
787 o.insert("type", Value::Str(t.into()));
788 Value::Object(o)
789 };
790 let out = dedup_hits_by_projection(vec![mk("1", "a"), mk("2", "b"), mk("3", "a")]);
791 assert_eq!(out.len(), 2);
792 assert_eq!(hit_str(&out[0], "id"), "1");
793 assert_eq!(hit_str(&out[1], "id"), "2");
794 }
795
796 #[test]
797 fn top_level_bounds_must_be_literals() {
798 assert_eq!(const_bound(None, "limit").unwrap(), None);
799 assert_eq!(
800 const_bound(Some(&Expr::Lit(Value::Number(3.0))), "limit").unwrap(),
801 Some(3)
802 );
803 let e = const_bound(Some(&Expr::Lit(Value::Number(-1.0))), "offset").unwrap_err();
804 assert_eq!(e.code, "filter_invalid");
805 assert!(e.message.contains("top-level offset"));
806 }
807}