1use std::collections::HashMap;
49use anyhow::{bail, Result};
50use serde_json::{json, Value};
51
52use crate::db::Db;
53use crate::index::OrderedValue;
54use crate::store::Node;
55
56#[derive(Debug, Clone, PartialEq)]
59enum Tok {
60 Kw(String, String),
69 Ident(String), Str(String), Num(f64), Op(String), Punct(char), Eof,
75}
76
77struct Lexer<'a> {
78 src: &'a str,
79 pos: usize,
80}
81
82impl<'a> Lexer<'a> {
83 fn new(src: &'a str) -> Self { Self { src, pos: 0 } }
84
85 fn peek_char(&self) -> Option<char> { self.src[self.pos..].chars().next() }
86
87 fn skip_ws(&mut self) {
88 while let Some(c) = self.peek_char() {
89 if c.is_whitespace() { self.pos += c.len_utf8(); } else { break; }
90 }
91 }
92
93 fn next_tok(&mut self) -> Tok {
94 self.skip_ws();
95 if self.pos >= self.src.len() { return Tok::Eof; }
96
97 let c = self.peek_char().unwrap();
98
99 if c == '"' {
111 self.pos += 1;
112 let mut s = String::new();
113 while let Some(ch) = self.peek_char() {
114 if ch == '"' {
115 break;
116 }
117 if ch == '\\' {
118 let next = self.src[self.pos + 1..].chars().next();
122 if next == Some('"') {
123 s.push('"');
124 self.pos += 1 + 1; continue;
126 }
127 }
128 s.push(ch);
129 self.pos += ch.len_utf8();
130 }
131 if self.peek_char() == Some('"') {
132 self.pos += 1;
133 }
134 return Tok::Str(s);
135 }
136
137 if self.pos + 2 < self.src.len() && &self.src[self.pos..self.pos + 3] == "!~*" {
141 self.pos += 3;
142 return Tok::Op("!~*".to_string());
143 }
144
145 if self.pos + 1 < self.src.len() {
147 let two = &self.src[self.pos..self.pos+2];
148 if matches!(two, "!=" | ">=" | "<=" | "!~" | "~*") {
149 self.pos += 2;
150 return Tok::Op(two.to_string());
151 }
152 }
153
154 if matches!(c, '=' | '>' | '<' | '~') {
156 self.pos += 1;
157 return Tok::Op(c.to_string());
158 }
159
160 if matches!(c, '(' | ')' | ',') {
165 self.pos += 1;
166 return Tok::Punct(c);
167 }
168
169 if c.is_ascii_digit() || (c == '-' && self.src[self.pos+1..].starts_with(|d: char| d.is_ascii_digit())) {
171 let start = self.pos;
172 if c == '-' { self.pos += 1; }
173 while let Some(d) = self.peek_char() {
174 if d.is_ascii_digit() || d == '.' { self.pos += 1; } else { break; }
175 }
176 let n: f64 = self.src[start..self.pos].parse().unwrap_or(0.0);
177 return Tok::Num(n);
178 }
179
180 if c.is_alphabetic() || c == '_' {
182 let start = self.pos;
183 while let Some(ch) = self.peek_char() {
184 if ch.is_alphanumeric() || ch == '_' || ch == '.' || ch == ':' {
185 self.pos += ch.len_utf8();
186 } else { break; }
187 }
188 let word = &self.src[start..self.pos];
189 let upper = word.to_uppercase();
190 let keywords = ["FROM","AS","OF","VALID","WHERE","AND","OR","ORDER","BY",
191 "ASC","DESC","LIMIT","OFFSET","GROUP","HAVING",
192 "COUNT","SUM","AVG","MIN","MAX",
193 "TRACE","TRAVERSE","REVERSE","SEARCH","NOT","NULL","TRUE","FALSE",
194 "IN","BETWEEN","LIKE","ILIKE","IS"];
195 if keywords.contains(&upper.as_str()) {
196 return Tok::Kw(upper, word.to_string());
197 }
198 return Tok::Ident(word.to_string());
199 }
200
201 self.pos += c.len_utf8();
203 self.next_tok()
204 }
205
206 fn tokenize(&mut self) -> Vec<Tok> {
207 let mut toks = vec![];
208 loop {
209 let t = self.next_tok();
210 if t == Tok::Eof { break; }
211 toks.push(t);
212 }
213 toks
214 }
215}
216
217#[derive(Debug, Clone)]
226pub enum Pred {
227 Cmp { field: String, op: String, value: Value },
229 In { field: String, values: Vec<Value>, negated: bool },
231 Between { field: String, low: Value, high: Value, negated: bool },
233 Like { field: String, pattern: String, negated: bool, ci: bool },
236 Regex { field: String, pattern: String, negated: bool, ci: bool },
255 IsNull { field: String, negated: bool },
257 And(Vec<Pred>),
258 Or(Vec<Pred>),
259 Not(Box<Pred>),
260}
261
262#[derive(Debug, Clone, PartialEq)]
263pub enum GroupAgg { Count, Sum, Avg, Min, Max }
264
265impl GroupAgg {
266 fn name(&self) -> &'static str {
267 match self {
268 GroupAgg::Count => "count", GroupAgg::Sum => "sum",
269 GroupAgg::Avg => "avg", GroupAgg::Min => "min",
270 GroupAgg::Max => "max",
271 }
272 }
273}
274
275#[derive(Debug, Clone, PartialEq)]
280pub struct OrderKey {
281 pub field: String,
282 pub desc: bool,
283}
284
285#[derive(Debug, Clone)]
292pub struct Aggregate {
293 pub group_field: Option<String>,
294 pub agg: GroupAgg,
295 pub agg_field: Option<String>,
297}
298
299#[derive(Debug, Clone)]
300pub struct Query {
301 pub coll: String,
302 pub as_of: Option<u64>,
303 pub valid_as_of: Option<String>,
304 pub where_: Option<Pred>,
305 pub search: Option<String>,
306 pub order_by: Vec<OrderKey>,
307 pub limit: Option<usize>,
308 pub offset: Option<usize>,
309 pub aggregate: Option<Aggregate>,
310 pub having: Option<Pred>,
314 pub trace: Option<String>, pub trace_rev: bool,
316 pub traverse: Option<String>, }
318
319struct Parser { toks: Vec<Tok>, pos: usize }
322
323impl Parser {
324 fn new(toks: Vec<Tok>) -> Self { Self { toks, pos: 0 } }
325
326 fn peek(&self) -> &Tok { self.toks.get(self.pos).unwrap_or(&Tok::Eof) }
327 fn advance(&mut self) -> Tok { let t = self.peek().clone(); self.pos += 1; t }
328
329 fn expect_kw(&mut self, kw: &str) -> Result<()> {
330 match self.advance() {
331 Tok::Kw(k, _) if k == kw => Ok(()),
332 other => bail!("expected keyword {}, got {:?}", kw, other),
333 }
334 }
335
336 fn parse_value(&mut self) -> Result<Value> {
343 Ok(match self.advance() {
344 Tok::Str(s) => Value::String(s),
345 Tok::Num(n) => json!(n),
346 Tok::Kw(k, _) if k == "NULL" => Value::Null,
347 Tok::Kw(k, _) if k == "TRUE" => Value::Bool(true),
348 Tok::Kw(k, _) if k == "FALSE" => Value::Bool(false),
349 Tok::Ident(s) => Value::String(s),
350 other => bail!("expected a value (string, number, TRUE, FALSE or NULL), got {:?}", other),
351 })
352 }
353
354 fn peek_kw(&self, kw: &str) -> bool {
355 matches!(self.peek(), Tok::Kw(k, _) if k == kw)
356 }
357
358 fn eat_kw(&mut self, kw: &str) -> bool {
359 if self.peek_kw(kw) { self.advance(); true } else { false }
360 }
361
362 fn expect_punct(&mut self, c: char) -> Result<()> {
363 match self.advance() {
364 Tok::Punct(p) if p == c => Ok(()),
365 other => bail!("expected '{}', got {:?}", c, other),
366 }
367 }
368
369 fn parse_agg_kw(&mut self) -> Result<GroupAgg> {
370 Ok(match self.advance() {
371 Tok::Kw(a, _) if a == "COUNT" => GroupAgg::Count,
372 Tok::Kw(a, _) if a == "SUM" => GroupAgg::Sum,
373 Tok::Kw(a, _) if a == "AVG" => GroupAgg::Avg,
374 Tok::Kw(a, _) if a == "MIN" => GroupAgg::Min,
375 Tok::Kw(a, _) if a == "MAX" => GroupAgg::Max,
376 other => bail!("expected an aggregate (COUNT/SUM/AVG/MIN/MAX), got {:?}", other),
377 })
378 }
379
380 fn parse_field(&mut self, ctx: &str) -> Result<String> {
381 match self.advance() {
382 Tok::Ident(s) | Tok::Kw(_, s) => Ok(s),
383 other => bail!("{}: expected field name, got {:?}", ctx, other),
384 }
385 }
386
387 fn parse_pred(&mut self) -> Result<Pred> { self.parse_or() }
390
391 fn parse_or(&mut self) -> Result<Pred> {
392 let mut terms = vec![self.parse_and()?];
393 while self.eat_kw("OR") {
394 terms.push(self.parse_and()?);
395 }
396 Ok(if terms.len() == 1 { terms.pop().unwrap() } else { Pred::Or(terms) })
397 }
398
399 fn parse_and(&mut self) -> Result<Pred> {
400 let mut terms = vec![self.parse_not()?];
401 while self.peek_kw("AND") {
402 self.advance();
405 terms.push(self.parse_not()?);
406 }
407 Ok(if terms.len() == 1 { terms.pop().unwrap() } else { Pred::And(terms) })
408 }
409
410 fn parse_not(&mut self) -> Result<Pred> {
411 if self.eat_kw("NOT") {
412 return Ok(Pred::Not(Box::new(self.parse_not()?)));
413 }
414 self.parse_primary()
415 }
416
417 fn parse_primary(&mut self) -> Result<Pred> {
418 if matches!(self.peek(), Tok::Punct('(')) {
419 self.advance();
420 let inner = self.parse_pred()?;
421 self.expect_punct(')')?;
422 return Ok(inner);
423 }
424 self.parse_comparison()
425 }
426
427 fn parse_comparison(&mut self) -> Result<Pred> {
428 let field = self.parse_field("WHERE")?;
429
430 if self.eat_kw("IS") {
432 let negated = self.eat_kw("NOT");
433 if !self.eat_kw("NULL") {
434 bail!("WHERE: expected NULL after IS{}", if negated { " NOT" } else { "" });
435 }
436 return Ok(Pred::IsNull { field, negated });
437 }
438
439 let negated = self.eat_kw("NOT");
441
442 if self.eat_kw("IN") {
443 self.expect_punct('(')?;
444 let mut values = vec![];
445 loop {
446 values.push(self.parse_value()?);
447 if matches!(self.peek(), Tok::Punct(',')) { self.advance(); continue; }
448 break;
449 }
450 self.expect_punct(')')?;
451 if values.is_empty() {
452 bail!("WHERE: IN () needs at least one value");
453 }
454 return Ok(Pred::In { field, values, negated });
455 }
456
457 if self.eat_kw("BETWEEN") {
458 let low = self.parse_value()?;
459 if !self.eat_kw("AND") {
460 bail!("WHERE: BETWEEN expects AND between its bounds");
461 }
462 let high = self.parse_value()?;
463 return Ok(Pred::Between { field, low, high, negated });
464 }
465
466 let ci = self.peek_kw("ILIKE");
467 if ci || self.peek_kw("LIKE") {
468 self.advance();
469 let pattern = match self.advance() {
470 Tok::Str(s) => s,
471 Tok::Ident(s) => s,
472 other => bail!("WHERE: LIKE expects a pattern string, got {:?}", other),
473 };
474 return Ok(Pred::Like { field, pattern, negated, ci });
475 }
476
477 if negated {
478 bail!("WHERE: NOT must be followed by IN, BETWEEN, LIKE or ILIKE \
479 (use `NOT (field = value)` or `field != value` to negate a comparison)");
480 }
481
482 let op = match self.advance() {
483 Tok::Op(s) => s,
484 other => bail!("WHERE: expected operator, got {:?}", other),
485 };
486
487 if matches!(op.as_str(), "~" | "~*" | "!~" | "!~*") {
492 let pattern = match self.advance() {
493 Tok::Str(s) => s,
494 Tok::Ident(s) => s,
495 other => bail!("WHERE: {} expects a pattern string, got {:?}", op, other),
496 };
497 if let Some(why) = regex_error(&pattern) {
501 bail!("WHERE: {} — in {:?}. The supported subset is ^ $ . | ( ) \
502 [ ] * + ? and literal text. Matching the rest approximately \
503 would silently include or exclude rows, so it is refused \
504 instead", why, pattern);
505 }
506 return Ok(Pred::Regex {
507 field,
508 pattern,
509 negated: op.starts_with('!'),
510 ci: op.ends_with('*'),
511 });
512 }
513
514 let value = self.parse_value()?;
515 Ok(Pred::Cmp { field, op, value })
516 }
517
518 fn parse(&mut self) -> Result<Query> {
519 self.expect_kw("FROM")?;
520 let coll = match self.advance() {
521 Tok::Ident(s) | Tok::Kw(_, s) => s,
522 other => bail!("expected collection name, got {:?}", other),
523 };
524
525 let mut q = Query {
526 coll, as_of: None, valid_as_of: None,
527 where_: None, search: None,
528 order_by: vec![],
529 limit: None, offset: None,
530 aggregate: None, having: None,
531 trace: None, trace_rev: false,
532 traverse: None,
533 };
534
535 loop {
536 match self.peek() {
537 Tok::Eof => break,
538
539 Tok::Kw(k, _) if k == "AS" => {
540 self.advance();
541 self.expect_kw("OF")?;
542 match self.advance() {
543 Tok::Num(n) => q.as_of = Some(n as u64),
544 Tok::Str(sx) => {
550 let moment = crate::wallclock::WallClock::parse(&sx)
551 .map_err(|e| anyhow::anyhow!(
552 "AS OF could not read {:?} as a datetime: {}", sx, e))?;
553 q.as_of = Some(moment.as_marker());
554 }
555 other => bail!(
556 "AS OF expects a sequence number or a quoted datetime, got {:?}",
557 other),
558 }
559 }
560
561 Tok::Kw(k, _) if k == "VALID" => {
562 self.advance();
563 self.expect_kw("AS")?;
564 self.expect_kw("OF")?;
565 match self.advance() {
566 Tok::Str(s) => q.valid_as_of = Some(s),
567 other => bail!("VALID AS OF expects date string, got {:?}", other),
568 }
569 }
570
571 Tok::Kw(k, _) if k == "WHERE" => {
572 self.advance();
573 let pred = self.parse_pred()?;
574 q.where_ = Some(match q.where_.take() {
577 None => pred,
578 Some(prev) => Pred::And(vec![prev, pred]),
579 });
580 }
581
582 Tok::Kw(k, _) if k == "SEARCH" => {
583 self.advance();
584 match self.advance() {
585 Tok::Str(s) => q.search = Some(s),
586 other => bail!("SEARCH expects quoted string, got {:?}", other),
587 }
588 }
589
590 Tok::Kw(k, _) if k == "ORDER" => {
591 self.advance();
592 self.expect_kw("BY")?;
593 loop {
596 let field = self.parse_field("ORDER BY")?;
597 let desc = if self.eat_kw("DESC") {
601 true
602 } else {
603 self.eat_kw("ASC");
604 false
605 };
606 q.order_by.push(OrderKey { field, desc });
607 if matches!(self.peek(), Tok::Punct(',')) { self.advance(); continue; }
608 break;
609 }
610 }
611
612 Tok::Kw(k, _) if k == "LIMIT" => {
613 self.advance();
614 match self.advance() {
615 Tok::Num(n) if n >= 0.0 => q.limit = Some(n as usize),
616 other => bail!("LIMIT expects a non-negative number, got {:?}", other),
617 }
618 }
619
620 Tok::Kw(k, _) if k == "OFFSET" => {
621 self.advance();
622 match self.advance() {
623 Tok::Num(n) if n >= 0.0 => q.offset = Some(n as usize),
624 other => bail!("OFFSET expects a non-negative number, got {:?}", other),
625 }
626 }
627
628 Tok::Kw(k, _) if k == "HAVING" => {
629 self.advance();
630 let pred = self.parse_pred()?;
631 q.having = Some(match q.having.take() {
632 None => pred,
633 Some(prev) => Pred::And(vec![prev, pred]),
634 });
635 }
636
637 Tok::Kw(k, _) if matches!(k.as_str(), "COUNT" | "SUM" | "AVG" | "MIN" | "MAX") => {
640 let agg = self.parse_agg_kw()?;
641 let agg_field = match agg {
642 GroupAgg::Count => None,
643 _ => Some(self.parse_field("aggregate")?),
644 };
645 if q.aggregate.is_some() {
646 bail!("only one aggregate per query");
647 }
648 q.aggregate = Some(Aggregate { group_field: None, agg, agg_field });
649 }
650
651 Tok::Kw(k, _) if k == "GROUP" => {
652 self.advance();
653 self.expect_kw("BY")?;
654 let field = match self.advance() {
655 Tok::Ident(s) | Tok::Kw(_, s) => s,
656 other => bail!("GROUP BY: expected field, got {:?}", other),
657 };
658 let agg = if matches!(self.peek(),
663 Tok::Kw(a, _) if matches!(a.as_str(), "COUNT"|"SUM"|"AVG"|"MIN"|"MAX"))
664 {
665 self.parse_agg_kw()?
666 } else {
667 GroupAgg::Count
668 };
669 let agg_field = match agg {
676 GroupAgg::Count => None,
677 _ => Some(self.parse_field("GROUP BY aggregate")?),
678 };
679 if q.aggregate.is_some() {
680 bail!("only one aggregate per query");
681 }
682 q.aggregate = Some(Aggregate {
683 group_field: Some(field), agg, agg_field,
684 });
685 }
686
687 Tok::Kw(k, _) if k == "TRACE" => {
688 self.advance();
689 let edge = match self.advance() {
690 Tok::Ident(s) | Tok::Kw(_, s) => s,
691 other => bail!("TRACE: expected edge type, got {:?}", other),
692 };
693 q.trace = Some(edge);
694 if let Tok::Kw(k, _) = self.peek() {
695 if k == "REVERSE" { self.advance(); q.trace_rev = true; }
696 }
697 }
698
699 Tok::Kw(k, _) if k == "TRAVERSE" => {
700 self.advance();
701 let rel = match self.advance() {
702 Tok::Ident(s) | Tok::Kw(_, s) => s,
703 other => bail!("TRAVERSE: expected relation name, got {:?}", other),
704 };
705 q.traverse = Some(rel);
706 }
707
708 other => bail!(
711 "unexpected {:?} in query. Expected one of: AS OF, VALID AS OF, \
712 WHERE, SEARCH, ORDER BY, LIMIT, OFFSET, GROUP BY, HAVING, \
713 COUNT, SUM, AVG, MIN, MAX, TRACE, TRAVERSE",
714 other
715 ),
716 }
717 }
718
719 Ok(q)
720 }
721}
722
723fn field_value(node: &Node, field: &str) -> Value {
728 match field {
729 "_id" => Value::String(node.id.clone()),
730 "_coll" => Value::String(node.coll.clone()),
731 "_hash" => Value::String(node.hash.clone()),
732 "_seq" => json!(node.seq),
733 _ => node.data.get(field).cloned().unwrap_or(Value::Null),
734 }
735}
736
737fn cmp_op(a: &Value, op: &str, b: &Value) -> bool {
738 if matches!(op, "<" | "<=" | ">" | ">=") && a.is_null() {
759 return false;
760 }
761 let a = OrderedValue::from(a);
762 let b = OrderedValue::from(b);
763 match op {
764 "=" => a == b,
765 "!=" => a != b,
766 ">" => a > b,
767 "<" => a < b,
768 ">=" => a >= b,
769 "<=" => a <= b,
770 _ => false,
771 }
772}
773
774fn as_text(v: &Value) -> String {
778 match v {
779 Value::String(s) => s.clone(),
780 Value::Null => String::new(),
781 other => other.to_string(),
782 }
783}
784
785fn like_match(value: &str, pattern: &str, ci: bool) -> bool {
790 like_match_esc(value, pattern, ci, None)
791}
792
793fn like_match_esc(value: &str, pattern: &str, ci: bool, esc: Option<char>) -> bool {
805 let (v, p): (Vec<char>, Vec<char>) = if ci {
806 (value.to_lowercase().chars().collect(), pattern.to_lowercase().chars().collect())
807 } else {
808 (value.chars().collect(), pattern.chars().collect())
809 };
810
811 let esc = esc.map(|c| if ci { c.to_lowercase().next().unwrap_or(c) } else { c });
812
813 let pat: Vec<(char, bool)> = {
822 let mut out: Vec<(char, bool)> = Vec::with_capacity(p.len());
823 let mut i = 0usize;
824 while i < p.len() {
825 if Some(p[i]) == esc {
826 if i + 1 < p.len() {
827 out.push((p[i + 1], true));
828 i += 2;
829 } else {
830 out.push((p[i], true));
831 i += 1;
832 }
833 } else {
834 out.push((p[i], false));
835 i += 1;
836 }
837 }
838 out
839 };
840
841 let mut vi = 0usize;
842 let mut pi = 0usize;
843 let mut star: Option<(usize, usize)> = None;
845
846 while vi < v.len() {
847 let wild_any = pi < pat.len() && pat[pi].0 == '%' && !pat[pi].1;
848 let wild_one = pi < pat.len() && pat[pi].0 == '_' && !pat[pi].1;
849 if pi < pat.len() && (wild_one || pat[pi].0 == v[vi]) {
850 vi += 1;
851 pi += 1;
852 } else if wild_any {
853 star = Some((pi, vi));
854 pi += 1;
855 } else if let Some((sp, sv)) = star {
856 pi = sp + 1;
858 vi = sv + 1;
859 star = Some((sp, vi));
860 } else {
861 return false;
862 }
863 }
864 while pi < pat.len() && pat[pi].0 == '%' && !pat[pi].1 { pi += 1; }
867 pi == pat.len()
868}
869
870#[derive(Debug, Clone, PartialEq)]
889enum ReNode {
890 Char(char),
891 Any,
892 Class { items: Vec<(char, char)>, negated: bool },
894 Start,
895 End,
896 Group(Vec<Vec<ReItem>>),
897}
898
899#[derive(Debug, Clone, Copy, PartialEq)]
900enum ReQuant { One, Opt, Star, Plus }
901
902#[derive(Debug, Clone, PartialEq)]
903struct ReItem { node: ReNode, quant: ReQuant }
904
905fn regex_compile(pattern: &str) -> Result<Vec<Vec<ReItem>>, String> {
907 let p: Vec<char> = pattern.chars().collect();
908 let mut pos = 0usize;
909 let alt = regex_parse_alt(&p, &mut pos, 0)?;
910 if pos < p.len() {
911 return Err(format!("regex {:?} has an unmatched ')'", pattern));
913 }
914 Ok(alt)
915}
916
917fn regex_parse_alt(p: &[char], pos: &mut usize, depth: usize) -> Result<Vec<Vec<ReItem>>, String> {
918 let mut branches = vec![];
919 loop {
920 let mut seq: Vec<ReItem> = vec![];
921 while *pos < p.len() {
922 let c = p[*pos];
923 if c == '|' || c == ')' {
924 break;
925 }
926 *pos += 1;
927 let node = match c {
928 '.' => ReNode::Any,
929 '^' => ReNode::Start,
930 '$' => ReNode::End,
931 '(' => {
932 let inner = regex_parse_alt(p, pos, depth + 1)?;
933 if *pos >= p.len() || p[*pos] != ')' {
934 return Err("regex has an unmatched '('".to_string());
935 }
936 *pos += 1;
937 ReNode::Group(inner)
938 }
939 '[' => {
940 let negated = *pos < p.len() && p[*pos] == '^';
941 if negated {
942 *pos += 1;
943 }
944 let mut items = vec![];
945 let mut first = true;
946 loop {
947 if *pos >= p.len() {
948 return Err("regex has an unmatched '['".to_string());
949 }
950 let ch = p[*pos];
951 if ch == ']' && !first {
952 *pos += 1;
953 break;
954 }
955 first = false;
956 if ch == '[' && p.get(*pos + 1) == Some(&':') {
957 return Err(
958 "regex uses a POSIX character class like [[:alpha:]], \
959 which this engine does not implement".to_string(),
960 );
961 }
962 let lo = if ch == '\\' {
963 *pos += 1;
964 *p.get(*pos).ok_or("regex ends inside an escape")?
965 } else {
966 ch
967 };
968 *pos += 1;
969 if *pos + 1 < p.len() && p[*pos] == '-' && p[*pos + 1] != ']' {
971 let hi = p[*pos + 1];
972 *pos += 2;
973 if hi < lo {
974 return Err(format!("regex range {}-{} is reversed", lo, hi));
975 }
976 items.push((lo, hi));
977 } else {
978 items.push((lo, lo));
979 }
980 }
981 ReNode::Class { items, negated }
982 }
983 '{' | '}' => {
984 return Err(format!(
985 "regex uses {:?} (an interval like a{{2,3}}), which this engine \
986 does not implement", c
987 ))
988 }
989 '*' | '+' | '?' => {
990 return Err(format!("regex has a {:?} with nothing to repeat", c))
991 }
992 '\\' => {
993 let e = *p.get(*pos).ok_or("regex ends inside an escape")?;
994 *pos += 1;
995 if e.is_ascii_digit() {
996 return Err("regex uses a back-reference like \\1, which this \
997 engine does not implement".to_string());
998 }
999 if matches!(e, 'd' | 'D' | 'w' | 'W' | 's' | 'S' | 'b' | 'B') {
1000 return Err(format!(
1001 "regex uses the shorthand class \\{}, which this engine does \
1002 not implement — write the [..] class out", e
1003 ));
1004 }
1005 ReNode::Char(e)
1006 }
1007 other => ReNode::Char(other),
1008 };
1009 let quant = match p.get(*pos) {
1010 Some('*') => { *pos += 1; ReQuant::Star }
1011 Some('+') => { *pos += 1; ReQuant::Plus }
1012 Some('?') => { *pos += 1; ReQuant::Opt }
1013 _ => ReQuant::One,
1014 };
1015 if quant != ReQuant::One && matches!(node, ReNode::Start | ReNode::End) {
1016 return Err("regex repeats an anchor, which is meaningless".to_string());
1017 }
1018 seq.push(ReItem { node, quant });
1019 }
1020 branches.push(seq);
1021 if *pos < p.len() && p[*pos] == '|' {
1022 *pos += 1;
1023 continue;
1024 }
1025 if *pos < p.len() && p[*pos] == ')' && depth == 0 {
1026 return Err("regex has an unmatched ')'".to_string());
1027 }
1028 return Ok(branches);
1029 }
1030}
1031
1032fn re_class_hit(items: &[(char, char)], negated: bool, c: char) -> bool {
1033 items.iter().any(|(lo, hi)| *lo <= c && c <= *hi) != negated
1034}
1035
1036fn re_atom(node: &ReNode, t: &[char], pos: usize, k: &dyn Fn(usize) -> bool) -> bool {
1038 match node {
1039 ReNode::Char(c) => pos < t.len() && t[pos] == *c && k(pos + 1),
1040 ReNode::Any => pos < t.len() && k(pos + 1),
1041 ReNode::Class { items, negated } => {
1042 pos < t.len() && re_class_hit(items, *negated, t[pos]) && k(pos + 1)
1043 }
1044 ReNode::Start => pos == 0 && k(pos),
1045 ReNode::End => pos == t.len() && k(pos),
1046 ReNode::Group(alt) => alt.iter().any(|seq| re_seq(seq, t, pos, k)),
1047 }
1048}
1049
1050fn re_star(node: &ReNode, rest: &[ReItem], t: &[char], pos: usize, k: &dyn Fn(usize) -> bool) -> bool {
1052 re_atom(node, t, pos, &|p| p != pos && re_star(node, rest, t, p, k)) || re_seq(rest, t, pos, k)
1054}
1055
1056fn re_seq(seq: &[ReItem], t: &[char], pos: usize, k: &dyn Fn(usize) -> bool) -> bool {
1057 let Some(item) = seq.first() else { return k(pos) };
1058 let rest = &seq[1..];
1059 match item.quant {
1060 ReQuant::One => re_atom(&item.node, t, pos, &|p| re_seq(rest, t, p, k)),
1061 ReQuant::Opt => {
1062 re_atom(&item.node, t, pos, &|p| re_seq(rest, t, p, k)) || re_seq(rest, t, pos, k)
1063 }
1064 ReQuant::Star => re_star(&item.node, rest, t, pos, k),
1065 ReQuant::Plus => re_atom(&item.node, t, pos, &|p| re_star(&item.node, rest, t, p, k)),
1066 }
1067}
1068
1069fn regex_error(pattern: &str) -> Option<String> {
1075 regex_compile(pattern).err()
1076}
1077
1078fn regex_match(value: &str, pattern: &str, ci: bool) -> bool {
1084 let (v, p) = if ci {
1085 (value.to_lowercase(), pattern.to_lowercase())
1086 } else {
1087 (value.to_string(), pattern.to_string())
1088 };
1089 let Ok(alt) = regex_compile(&p) else { return false };
1090 let t: Vec<char> = v.chars().collect();
1091 let done = |_: usize| true;
1092 (0..=t.len()).any(|start| alt.iter().any(|seq| re_seq(seq, &t, start, &done)))
1093}
1094
1095pub fn regex_match_pub(value: &str, pattern: &str, ci: bool) -> bool {
1103 regex_match(value, pattern, ci)
1104}
1105
1106pub fn regex_error_pub(pattern: &str) -> Option<String> {
1109 regex_error(pattern)
1110}
1111
1112pub fn like_match_pub(value: &str, pattern: &str, ci: bool) -> bool {
1114 like_match_esc(value, pattern, ci, None)
1115}
1116
1117pub fn like_match_escape_pub(value: &str, pattern: &str, ci: bool, esc: char) -> bool {
1125 like_match_esc(value, pattern, ci, Some(esc))
1126}
1127
1128fn eval_pred_with(get: &dyn Fn(&str) -> Value, pred: &Pred) -> bool {
1135 match pred {
1136 Pred::Cmp { field, op, value } => cmp_op(&get(field), op, value),
1137
1138 Pred::In { field, values, negated } => {
1139 let fv = get(field);
1140 let hit = values.iter().any(|v| cmp_op(&fv, "=", v));
1141 hit != *negated
1142 }
1143
1144 Pred::Between { field, low, high, negated } => {
1145 let fv = get(field);
1146 let hit = cmp_op(&fv, ">=", low) && cmp_op(&fv, "<=", high);
1148 hit != *negated
1149 }
1150
1151 Pred::Like { field, pattern, negated, ci } => {
1152 let fv = get(field);
1153 if fv.is_null() { return false; }
1157 let hit = like_match(&as_text(&fv), pattern, *ci);
1158 hit != *negated
1159 }
1160
1161 Pred::Regex { field, pattern, negated, ci } => {
1162 let fv = get(field);
1163 if fv.is_null() { return false; }
1168 let hit = regex_match(&as_text(&fv), pattern, *ci);
1169 hit != *negated
1170 }
1171
1172 Pred::IsNull { field, negated } => {
1173 get(field).is_null() != *negated
1177 }
1178
1179 Pred::And(terms) => terms.iter().all(|t| eval_pred_with(get, t)),
1180 Pred::Or(terms) => terms.iter().any(|t| eval_pred_with(get, t)),
1181 Pred::Not(inner) => !eval_pred_with(get, inner),
1182 }
1183}
1184
1185fn eval_pred(node: &Node, pred: &Pred) -> bool {
1186 eval_pred_with(&|f| field_value(node, f), pred)
1187}
1188
1189fn eval_pred_json(obj: &Value, pred: &Pred) -> bool {
1191 eval_pred_with(&|f| obj.get(f).cloned().unwrap_or(Value::Null), pred)
1192}
1193
1194fn sort_by_keys<T>(rows: &mut [T], keys: &[OrderKey], get: impl Fn(&T, &str) -> Value) {
1197 rows.sort_by(|a, b| {
1198 for k in keys {
1199 let av = OrderedValue::from(&get(a, &k.field));
1200 let bv = OrderedValue::from(&get(b, &k.field));
1201 let ord = if k.desc { bv.cmp(&av) } else { av.cmp(&bv) };
1202 if ord != std::cmp::Ordering::Equal {
1203 return ord;
1204 }
1205 }
1206 std::cmp::Ordering::Equal
1207 });
1208}
1209
1210fn paginate<T>(rows: Vec<T>, offset: Option<usize>, limit: Option<usize>) -> Vec<T> {
1215 let mut it = rows;
1216 if let Some(off) = offset {
1217 if off >= it.len() {
1218 return vec![];
1219 }
1220 it.drain(..off);
1221 }
1222 if let Some(n) = limit {
1223 it.truncate(n);
1224 }
1225 it
1226}
1227
1228fn aggregate_rows(rows: &[Node], spec: &Aggregate) -> Vec<Value> {
1241 struct Group { count: usize, nums: Vec<f64>, ints: Vec<i64>, all_int: bool }
1251
1252 let mut order: Vec<String> = vec![];
1255 let mut groups: HashMap<String, Group> = HashMap::new();
1256
1257 const WHOLE: &str = "";
1260
1261 for node in rows {
1262 let key = match spec.group_field {
1270 None => WHOLE.to_string(),
1271 Some(ref gf) => match field_value(node, gf) {
1272 Value::Null => "null".to_string(),
1273 v => as_text(&v),
1274 },
1275 };
1276 let entry = groups.entry(key.clone()).or_insert_with(|| {
1277 order.push(key.clone());
1278 Group { count: 0, nums: vec![], ints: vec![], all_int: true }
1279 });
1280 entry.count += 1;
1281 if let Some(ref af) = spec.agg_field {
1282 if let Value::Number(n) = field_value(node, af) {
1285 if let Some(i) = n.as_i64() {
1286 entry.ints.push(i);
1287 entry.nums.push(i as f64);
1288 } else if let Some(f) = n.as_f64() {
1289 entry.all_int = false;
1290 entry.nums.push(f);
1291 }
1292 }
1293 }
1294 }
1295
1296 if spec.group_field.is_none() && order.is_empty() {
1300 order.push(WHOLE.to_string());
1301 groups.insert(WHOLE.to_string(),
1302 Group { count: 0, nums: vec![], ints: vec![], all_int: true });
1303 }
1304
1305 order.into_iter().map(|k| {
1306 let g = &groups[&k];
1307 let mut obj = serde_json::Map::new();
1308 if let Some(ref gf) = spec.group_field {
1309 obj.insert(gf.clone(), Value::String(k.clone()));
1310 }
1311 obj.insert("count".to_string(), json!(g.count));
1312
1313 let int_path = g.all_int && !g.ints.is_empty();
1317 let agg_val: Value = match spec.agg {
1318 GroupAgg::Count => json!(g.count),
1319 _ if g.nums.is_empty() => Value::Null,
1320 GroupAgg::Sum if int_path => {
1323 match g.ints.iter().try_fold(0i64, |a, &b| a.checked_add(b)) {
1324 Some(t) => json!(t),
1325 None => json!(g.nums.iter().sum::<f64>()),
1326 }
1327 }
1328 GroupAgg::Min if int_path => json!(g.ints.iter().min().copied().unwrap()),
1329 GroupAgg::Max if int_path => json!(g.ints.iter().max().copied().unwrap()),
1330 GroupAgg::Sum => json!(g.nums.iter().sum::<f64>()),
1331 GroupAgg::Avg => json!(g.nums.iter().sum::<f64>() / g.nums.len() as f64),
1333 GroupAgg::Min => json!(g.nums.iter().cloned().fold(f64::INFINITY, f64::min)),
1334 GroupAgg::Max => json!(g.nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max)),
1335 };
1336
1337 if let Some(ref af) = spec.agg_field {
1339 obj.insert(format!("{}_{}", spec.agg.name(), af), agg_val.clone());
1340 }
1341 obj.insert("value".to_string(), agg_val);
1346 Value::Object(obj)
1347 }).collect()
1348}
1349
1350#[derive(Debug, Clone)]
1359enum IndexPlan {
1360 Range {
1362 field: String,
1363 low: Option<Value>,
1364 high: Option<Value>,
1365 low_incl: bool,
1366 high_incl: bool,
1367 },
1368 Values { field: String, values: Vec<Value> },
1370}
1371
1372impl IndexPlan {
1373 fn field(&self) -> &str {
1374 match self {
1375 IndexPlan::Range { field, .. } => field,
1376 IndexPlan::Values { field, .. } => field,
1377 }
1378 }
1379}
1380
1381fn collect_index_constraints(pred: &Pred, out: &mut Vec<IndexPlan>) {
1394 match pred {
1395 Pred::And(terms) => {
1396 for t in terms {
1397 collect_index_constraints(t, out);
1398 }
1399 }
1400
1401 Pred::Cmp { field, op, value } => {
1402 if field == "_id" {
1404 return;
1405 }
1406 match op.as_str() {
1407 "=" => out.push(IndexPlan::Values {
1408 field: field.clone(),
1409 values: vec![value.clone()],
1410 }),
1411 ">" | ">=" => out.push(IndexPlan::Range {
1412 field: field.clone(),
1413 low: Some(value.clone()),
1414 high: None,
1415 low_incl: op == ">=",
1416 high_incl: true,
1417 }),
1418 "<" | "<=" => out.push(IndexPlan::Range {
1419 field: field.clone(),
1420 low: None,
1421 high: Some(value.clone()),
1422 low_incl: true,
1423 high_incl: op == "<=",
1424 }),
1425 _ => {}
1428 }
1429 }
1430
1431 Pred::Between { field, low, high, negated: false } => {
1432 out.push(IndexPlan::Range {
1433 field: field.clone(),
1434 low: Some(low.clone()),
1435 high: Some(high.clone()),
1436 low_incl: true, high_incl: true,
1438 });
1439 }
1440
1441 Pred::In { field, values, negated: false } => {
1442 out.push(IndexPlan::Values {
1443 field: field.clone(),
1444 values: values.clone(),
1445 });
1446 }
1447
1448 _ => {}
1454 }
1455}
1456
1457fn choose_index_plan(db: &Db, coll: &str, pred: &Pred) -> Option<IndexPlan> {
1464 let mut raw = vec![];
1465 collect_index_constraints(pred, &mut raw);
1466 raw.retain(|p| db.has_sorted_index(coll, p.field()));
1467 if raw.is_empty() {
1468 return None;
1469 }
1470
1471 let mut merged: Vec<IndexPlan> = vec![];
1473 for plan in raw {
1474 let field = plan.field().to_string();
1475 let existing = merged.iter().position(|m| m.field() == field);
1476 match (existing, plan) {
1477 (None, p) => merged.push(p),
1478
1479 (Some(i), IndexPlan::Range { low, high, low_incl, high_incl, .. }) => {
1481 if let IndexPlan::Range {
1482 low: ref mut elow, high: ref mut ehigh,
1483 low_incl: ref mut eli, high_incl: ref mut ehi, ..
1484 } = merged[i] {
1485 if let Some(l) = low {
1486 let tighter = match elow {
1487 None => true,
1488 Some(cur) => OrderedValue::from(&l) > OrderedValue::from(&*cur),
1489 };
1490 if tighter { *elow = Some(l); *eli = low_incl; }
1491 }
1492 if let Some(h) = high {
1493 let tighter = match ehigh {
1494 None => true,
1495 Some(cur) => OrderedValue::from(&h) < OrderedValue::from(&*cur),
1496 };
1497 if tighter { *ehigh = Some(h); *ehi = high_incl; }
1498 }
1499 }
1500 }
1504
1505 (Some(i), p @ IndexPlan::Values { .. }) => {
1507 if matches!(merged[i], IndexPlan::Range { .. }) {
1508 merged[i] = p;
1509 }
1510 }
1511 }
1512 }
1513
1514 let mut best: Option<(usize, IndexPlan)> = None;
1518 for plan in merged {
1519 let card = match &plan {
1520 IndexPlan::Range { field, low, high, low_incl, high_incl } => db
1521 .range_cardinality(coll, field, low.as_ref(), high.as_ref(),
1522 *low_incl, *high_incl)
1523 .unwrap_or(usize::MAX),
1524 IndexPlan::Values { field, values } => values
1525 .iter()
1526 .map(|v| db.range_cardinality(coll, field, Some(v), Some(v), true, true)
1527 .unwrap_or(usize::MAX))
1528 .fold(0usize, |a, b| a.saturating_add(b)),
1529 };
1530 if best.as_ref().map(|(c, _)| card < *c).unwrap_or(true) {
1531 best = Some((card, plan));
1532 }
1533 }
1534 best.map(|(_, p)| p)
1535}
1536
1537fn id_point_lookup(pred: &Pred) -> Option<String> {
1538 match pred {
1539 Pred::Cmp { field, op, value } if field == "_id" && op == "=" => {
1540 if let Value::String(s) = value { Some(s.clone()) } else { None }
1541 }
1542 Pred::And(terms) => terms.iter().find_map(id_point_lookup),
1543 _ => None,
1544 }
1545}
1546
1547use crate::relation::{matches_valid_as_of, node_contains_text};
1554
1555pub fn node_to_json(node: &Node) -> Value {
1559 let mut obj = if let Value::Object(m) = &node.data {
1560 m.clone()
1561 } else {
1562 serde_json::Map::new()
1563 };
1564 obj.insert("_id".to_string(), Value::String(node.id.clone()));
1565 obj.insert("_hash".to_string(), Value::String(node.hash.clone()));
1566 obj.insert("_seq".to_string(), json!(node.seq));
1567 obj.insert("_coll".to_string(), Value::String(node.coll.clone()));
1568 if let Some(ref vf) = node.valid_from {
1569 obj.insert("_valid_from".to_string(), Value::String(vf.clone()));
1570 }
1571 if let Some(ref vt) = node.valid_to {
1572 obj.insert("_valid_to".to_string(), Value::String(vt.clone()));
1573 }
1574 if !node.caused_by.is_empty() {
1575 obj.insert("_caused_by".to_string(), Value::Array(
1576 node.caused_by.iter().map(|h| Value::String(h.clone())).collect()
1577 ));
1578 }
1579 Value::Object(obj)
1580}
1581
1582pub fn parse(nql: &str) -> Result<Query> {
1591 let mut lexer = Lexer::new(nql);
1592 let toks = lexer.tokenize();
1593 let mut parser = Parser::new(toks);
1594 parser.parse()
1595}
1596
1597pub fn execute(db: &Db, nql: &str) -> Result<Vec<Value>> {
1598 let mut q = parse(nql)?;
1601
1602 resolve_as_of_in(db, &mut q)?;
1607
1608 let id_eq_fast_path: Option<String> = if q.as_of.is_none() && q.trace.is_none() {
1615 q.where_.as_ref().and_then(id_point_lookup)
1616 } else { None };
1617
1618 let candidates: Vec<Node> = if let Some(ref target_id) = id_eq_fast_path {
1619 db.get(&q.coll, target_id).into_iter().collect()
1621 } else if let Some(seq_target) = q.as_of {
1622 db.list_ids_including_deleted(&q.coll).into_iter()
1632 .filter_map(|id| db.get_as_of(&q.coll, &id, seq_target))
1633 .collect()
1634 } else if let Some(plan) = q.where_.as_ref()
1635 .filter(|_| q.as_of.is_none())
1644 .and_then(|p| choose_index_plan(db, &q.coll, p))
1645 {
1646 let got = match &plan {
1649 IndexPlan::Range { field, low, high, low_incl, high_incl } => db.range_scan(
1650 &q.coll, field, low.as_ref(), high.as_ref(), *low_incl, *high_incl),
1651 IndexPlan::Values { field, values } => db.index_lookup(&q.coll, field, values),
1652 };
1653 match got {
1654 Some(nodes) => nodes,
1655 None => db.list(&q.coll),
1658 }
1659 } else if q.order_by.len() == 1 && q.aggregate.is_none() {
1660 let key = &q.order_by[0];
1675 let has_post_filters = q.where_.is_some()
1676 || q.search.is_some()
1677 || q.valid_as_of.is_some();
1678 let limit = if has_post_filters {
1679 9_999_999
1680 } else {
1681 match q.limit {
1685 Some(n) => n.saturating_add(q.offset.unwrap_or(0)),
1686 None => 9_999_999,
1687 }
1688 };
1689 if key.desc {
1690 db.order_by_desc(&q.coll, &key.field, limit)
1691 } else {
1692 db.order_by_asc(&q.coll, &key.field, limit)
1693 }
1694 } else if let (Some(n), true) = (q.limit, q.where_.is_none()
1695 && q.search.is_none() && q.trace.is_none()
1696 && q.traverse.is_none() && q.aggregate.is_none()
1697 && q.order_by.is_empty() && q.offset.is_none()
1698 && q.valid_as_of.is_none()) {
1699 db.id_index
1704 .list_ids(&q.coll)
1705 .into_iter()
1706 .take(n)
1707 .filter_map(|id| db.get(&q.coll, &id))
1708 .collect()
1709 } else {
1710 db.list(&q.coll)
1712 };
1713
1714 let mut rows: Vec<Node> = candidates.into_iter()
1717 .filter(|n| q.where_.as_ref().map(|p| eval_pred(n, p)).unwrap_or(true))
1718 .filter(|n| q.valid_as_of.as_deref()
1719 .map(|d| matches_valid_as_of(n, d))
1720 .unwrap_or(true))
1721 .filter(|n| q.search.as_deref()
1722 .map(|t| node_contains_text(n, t))
1723 .unwrap_or(true))
1724 .collect();
1725
1726 if let Some(ref _edge_type) = q.trace {
1729 let limit = q.limit.unwrap_or(1000);
1730 let mut traced: Vec<Node> = vec![];
1731 for root in &rows {
1732 let chain = db.trace(&root.hash, q.trace_rev, limit);
1733 traced.extend(chain);
1734 }
1735 rows = traced;
1736 }
1737
1738 if let Some(ref rel) = q.traverse {
1741 let mut traversed: Vec<Node> = vec![];
1742 for root in &rows {
1743 let frm = format!("{}:{}", root.coll, root.id);
1744 let neighbors = db.neighbors(&frm, rel);
1745 traversed.extend(neighbors);
1746 }
1747 rows = traversed;
1748 }
1749
1750 if let Some(ref spec) = q.aggregate {
1768 let mut out = aggregate_rows(&rows, spec);
1769
1770 if let Some(ref pred) = q.having {
1773 out.retain(|row| eval_pred_json(row, pred));
1774 }
1775
1776 if !q.order_by.is_empty() {
1777 sort_by_keys(&mut out, &q.order_by,
1778 |row, f| row.get(f).cloned().unwrap_or(Value::Null));
1779 } else if let Some(ref gf) = spec.group_field {
1780 let gf = gf.clone();
1788 out.sort_by(|a, b| {
1789 as_text(&a.get(&gf).cloned().unwrap_or(Value::Null))
1790 .cmp(&as_text(&b.get(&gf).cloned().unwrap_or(Value::Null)))
1791 });
1792 }
1793
1794 return Ok(paginate(out, q.offset, q.limit));
1795 }
1796
1797 if q.having.is_some() {
1798 bail!("HAVING requires an aggregate — add GROUP BY <field>, or use WHERE \
1799 to filter individual rows");
1800 }
1801
1802 if !q.order_by.is_empty() {
1805 let index_path_held = q.order_by.len() == 1
1809 && q.as_of.is_none()
1810 && q.where_.is_none()
1811 && q.search.is_none()
1812 && q.valid_as_of.is_none()
1813 && q.trace.is_none()
1814 && q.traverse.is_none();
1815 if !index_path_held {
1816 sort_by_keys(&mut rows, &q.order_by,
1817 |n, f| field_value(n, f));
1818 }
1819 }
1820
1821 let rows = paginate(rows, q.offset, q.limit);
1824
1825 Ok(rows.into_iter().map(|n| node_to_json(&n)).collect())
1828}
1829
1830fn resolve_as_of_in(db: &Db, q: &mut Query) -> Result<()> {
1841 if let Some(marker) = q.as_of {
1842 q.as_of = Some(
1843 crate::relation::resolve_as_of(db, marker).map_err(|e| anyhow::anyhow!("{}", e))?,
1844 );
1845 }
1846 Ok(())
1847}
1848
1849pub fn query(db: &Db, nql: &str) -> Result<(Vec<Value>, usize)> {
1850 let rows = execute(db, nql)?;
1851 let count = rows.len();
1852 Ok((rows, count))
1853}
1854
1855pub fn query_rows(rows: Vec<Value>, nql: &str) -> Result<Vec<Value>> {
1876 let q = parse(nql)?;
1877
1878 for (unsupported, clause) in [
1879 (q.as_of.is_some(), "AS OF"),
1880 (q.valid_as_of.is_some(), "VALID AS OF"),
1881 (q.trace.is_some(), "TRACE"),
1882 (q.traverse.is_some(), "TRAVERSE"),
1883 (q.search.is_some(), "SEARCH"),
1884 (q.aggregate.is_some(), "an aggregate"),
1885 (q.having.is_some(), "HAVING"),
1886 ] {
1887 if unsupported {
1888 bail!("{} is not supported on the catalogue table {:?} — a catalogue \
1889 is synthesised from the current database, so it has no history, \
1890 no causal edges and nothing to aggregate. Query the collection \
1891 itself for those", clause, q.coll);
1892 }
1893 }
1894
1895 let get = |row: &Value, field: &str| -> Value {
1896 row.get(field).cloned().unwrap_or(Value::Null)
1897 };
1898
1899 let mut kept: Vec<Value> = match &q.where_ {
1900 None => rows,
1901 Some(pred) => rows
1902 .into_iter()
1903 .filter(|r| eval_pred_with(&|f| get(r, f), pred))
1904 .collect(),
1905 };
1906
1907 if !q.order_by.is_empty() {
1908 sort_by_keys(&mut kept, &q.order_by, get);
1909 }
1910 Ok(paginate(kept, q.offset, q.limit))
1911}
1912
1913#[cfg(test)]
1914mod tests {
1915 use super::*;
1916 use tempfile::tempdir;
1917 use crate::db::Db;
1918
1919 fn setup() -> (tempfile::TempDir, Db) {
1924 let dir = tempdir().unwrap();
1925 let db = Db::open(dir.path(), None).unwrap();
1926 db.create_sorted_index("blocks", "height");
1927 for h in 1u64..=5 {
1928 db.put("blocks", &h.to_string(),
1929 serde_json::json!({"height": h, "hash": format!("000{}", h), "n_tx": h * 2}),
1930 vec![], None, None).unwrap();
1931 }
1932 (dir, db)
1933 }
1934
1935 #[test]
1936 fn from_all() {
1937 let (_tmp, db) = setup();
1938 let (rows, count) = query(&db, "FROM blocks").unwrap();
1939 assert_eq!(count, 5);
1940 let _ = rows;
1941 }
1942
1943 #[test]
1944 fn where_eq() {
1945 let (_tmp, db) = setup();
1946 let (rows, count) = query(&db, r#"FROM blocks WHERE _id = "3""#).unwrap();
1947 assert_eq!(count, 1);
1948 assert_eq!(rows[0]["_id"], "3");
1949 }
1950
1951 #[test]
1952 fn order_by_limit() {
1953 let (_tmp, db) = setup();
1954 let (rows, count) = query(&db, "FROM blocks ORDER BY height ASC LIMIT 3").unwrap();
1955 assert_eq!(count, 3);
1956 assert_eq!(rows[0]["height"], 1);
1957 assert_eq!(rows[2]["height"], 3);
1958 }
1959
1960 #[test]
1961 fn order_by_desc() {
1962 let (_tmp, db) = setup();
1963 let (rows, _) = query(&db, "FROM blocks ORDER BY height DESC LIMIT 2").unwrap();
1964 assert_eq!(rows[0]["height"], 5);
1965 }
1966
1967 #[test]
1968 fn where_gt() {
1969 let (_tmp, db) = setup();
1970 let (rows, _) = query(&db, "FROM blocks WHERE height > 3").unwrap();
1971 assert_eq!(rows.len(), 2);
1972 }
1973
1974 #[test]
1981 fn where_order_limit_does_not_truncate_before_filter() {
1982 let (_tmp, db) = setup();
1983 let (rows, count) =
1984 query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height LIMIT 2").unwrap();
1985 assert_eq!(count, 2, "both matching rows must survive the limit");
1986 let heights: Vec<u64> = rows.iter()
1987 .filter_map(|r| r["height"].as_u64())
1988 .collect();
1989 assert_eq!(heights, vec![4, 5]);
1990 let (rows_d, _) =
1992 query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height DESC LIMIT 1").unwrap();
1993 assert_eq!(rows_d.len(), 1);
1994 assert_eq!(rows_d[0]["height"], 5);
1995 }
1996
1997 fn setup_text() -> (tempfile::TempDir, Db) {
2005 let dir = tempdir().unwrap();
2006 let db = Db::open(dir.path(), None).unwrap();
2007 let rows = [
2008 ("1", serde_json::json!({"status": "open", "miner": "Acme Pool", "fee": 10})),
2009 ("2", serde_json::json!({"status": "pending", "miner": "acme solo", "fee": 20})),
2010 ("3", serde_json::json!({"status": "closed", "miner": "Zenith", "fee": 30})),
2011 ("4", serde_json::json!({"status": "open", "fee": 40})),
2012 ("5", serde_json::json!({"status": "voided", "miner": Value::Null, "fee": 50})),
2013 ];
2014 for (id, data) in rows {
2015 db.put("jobs", id, data, vec![], None, None).unwrap();
2016 }
2017 (dir, db)
2018 }
2019
2020 fn ids(rows: &[Value]) -> Vec<String> {
2021 let mut v: Vec<String> = rows.iter()
2022 .filter_map(|r| r["_id"].as_str().map(String::from))
2023 .collect();
2024 v.sort();
2025 v
2026 }
2027
2028 #[test]
2029 fn where_in_list() {
2030 let (_tmp, db) = setup();
2031 let (rows, _) = query(&db, "FROM blocks WHERE height IN (2, 4)").unwrap();
2032 assert_eq!(ids(&rows), vec!["2", "4"]);
2033 }
2034
2035 #[test]
2036 fn where_in_strings() {
2037 let (_tmp, db) = setup_text();
2038 let (rows, _) = query(&db, r#"FROM jobs WHERE status IN ("open", "closed")"#).unwrap();
2039 assert_eq!(ids(&rows), vec!["1", "3", "4"]);
2040 }
2041
2042 #[test]
2043 fn where_not_in() {
2044 let (_tmp, db) = setup();
2045 let (rows, _) = query(&db, "FROM blocks WHERE height NOT IN (1, 2, 3)").unwrap();
2046 assert_eq!(ids(&rows), vec!["4", "5"]);
2047 }
2048
2049 #[test]
2050 fn where_in_single_value_equals_eq() {
2051 let (_tmp, db) = setup();
2052 let (a, _) = query(&db, "FROM blocks WHERE height IN (3)").unwrap();
2053 let (b, _) = query(&db, "FROM blocks WHERE height = 3").unwrap();
2054 assert_eq!(ids(&a), ids(&b));
2055 }
2056
2057 #[test]
2058 fn where_between_is_inclusive() {
2059 let (_tmp, db) = setup();
2060 let (rows, _) = query(&db, "FROM blocks WHERE height BETWEEN 2 AND 4").unwrap();
2061 assert_eq!(ids(&rows), vec!["2", "3", "4"]);
2063 }
2064
2065 #[test]
2066 fn where_not_between() {
2067 let (_tmp, db) = setup();
2068 let (rows, _) = query(&db, "FROM blocks WHERE height NOT BETWEEN 2 AND 4").unwrap();
2069 assert_eq!(ids(&rows), vec!["1", "5"]);
2070 }
2071
2072 #[test]
2076 fn between_and_does_not_swallow_the_conjunction() {
2077 let (_tmp, db) = setup();
2078 let (rows, _) = query(
2079 &db, "FROM blocks WHERE height BETWEEN 2 AND 4 AND n_tx > 4").unwrap();
2080 assert_eq!(ids(&rows), vec!["3", "4"]);
2082 }
2083
2084 #[test]
2085 fn where_like_prefix_suffix_and_infix() {
2086 let (_tmp, db) = setup_text();
2087 let (pre, _) = query(&db, r#"FROM jobs WHERE miner LIKE "Acme%""#).unwrap();
2088 assert_eq!(ids(&pre), vec!["1"]);
2089 let (suf, _) = query(&db, r#"FROM jobs WHERE miner LIKE "%Pool""#).unwrap();
2090 assert_eq!(ids(&suf), vec!["1"]);
2091 let (inf, _) = query(&db, r#"FROM jobs WHERE status LIKE "%pen%""#).unwrap();
2092 assert_eq!(ids(&inf), vec!["1", "2", "4"]); }
2094
2095 #[test]
2096 fn where_like_underscore_matches_exactly_one_char() {
2097 let (_tmp, db) = setup_text();
2098 let (rows, _) = query(&db, r#"FROM jobs WHERE status LIKE "open_""#).unwrap();
2099 assert!(rows.is_empty(), "`open_` must not match the 4-char value `open`");
2100 let (rows2, _) = query(&db, r#"FROM jobs WHERE status LIKE "ope_""#).unwrap();
2101 assert_eq!(ids(&rows2), vec!["1", "4"]);
2102 }
2103
2104 #[test]
2105 fn where_ilike_is_case_insensitive_and_like_is_not() {
2106 let (_tmp, db) = setup_text();
2107 let (ci, _) = query(&db, r#"FROM jobs WHERE miner ILIKE "acme%""#).unwrap();
2108 assert_eq!(ci.len(), 2, "ILIKE matches both `Acme Pool` and `acme solo`");
2109 let (cs, _) = query(&db, r#"FROM jobs WHERE miner LIKE "acme%""#).unwrap();
2110 assert_eq!(ids(&cs), vec!["2"], "LIKE stays case-sensitive");
2111 }
2112
2113 #[test]
2116 fn like_backtracks_across_multiple_wildcards() {
2117 assert!(like_match("abcabcabd", "%abc%abd", false));
2118 assert!(like_match("aaa", "%a", false));
2119 assert!(like_match("", "%", false));
2120 assert!(like_match("x", "%%%", false));
2121 assert!(!like_match("abc", "%abd", false));
2122 assert!(!like_match("ab", "ab_", false));
2123 assert!(like_match("héllo wörld", "h_llo w%d", false));
2124 }
2125
2126 #[test]
2129 fn regex_anchors_behave_as_posix_says() {
2130 assert!(regex_match("pg_catalog", "^pg_", false));
2132 assert!(regex_match("pg_toast_1", "^pg_toast", false));
2133 assert!(!regex_match("public", "^pg_", false));
2134 assert!(!regex_match("my_pg_stuff", "^pg_", false));
2137
2138 assert!(regex_match("report.sql", "sql$", false));
2139 assert!(!regex_match("sql_report", "sql$", false));
2140 assert!(regex_match("public", "^public$", false));
2142 assert!(!regex_match("public2", "^public$", false));
2143 assert!(regex_match("xxpg_yy", "pg_", false));
2145 assert!(!regex_match("xxqg_yy", "pg_", false));
2146 }
2147
2148 #[test]
2149 fn regex_dot_matches_exactly_one_character() {
2150 assert!(regex_match("abc", "a.c", false));
2151 assert!(!regex_match("ac", "a.c", false), "`.` is one char, not zero");
2152 assert!(!regex_match("abbc", "a.c", false), "`.` is one char, not many");
2153 assert!(regex_match("héllo", "h.llo", false));
2155 }
2156
2157 #[test]
2158 fn regex_case_insensitivity_is_opt_in() {
2159 assert!(regex_match("PG_CATALOG", "^pg_", true));
2160 assert!(!regex_match("PG_CATALOG", "^pg_", false),
2161 "`~` is case SENSITIVE; only `~*` folds case");
2162 }
2163
2164 #[test]
2165 fn an_empty_regex_matches_anything() {
2166 assert!(regex_match("anything", "", false));
2168 assert!(regex_match("", "", false));
2169 assert!(regex_match("x", "$", false));
2170 }
2171
2172 #[test]
2173 fn regex_groups_alternation_classes_and_quantifiers_match_as_ERE_says() {
2174 assert!(regex_match("orders", "^(orders)$", false));
2177 assert!(!regex_match("orders2", "^(orders)$", false));
2178 assert!(regex_match("orders", "^(ord.*)$", false));
2179 assert!(regex_match("ord", "^(ord.*)$", false), "`.*` may match nothing");
2180 assert!(!regex_match("xord", "^(ord.*)$", false));
2181 assert!(regex_match("drivers", "^(orders|drivers)$", false));
2183 assert!(!regex_match("riders", "^(orders|drivers)$", false));
2184 assert!(regex_match("b", "a|b", false));
2185 assert!(regex_match("aaab", "^a+b$", false));
2187 assert!(!regex_match("b", "^a+b$", false));
2188 assert!(regex_match("b", "^a*b$", false));
2189 assert!(regex_match("ab", "^a?b$", false));
2190 assert!(!regex_match("aab", "^a?b$", false));
2191 assert!(regex_match("aXb", "^a.+b$", false));
2192 assert!(regex_match("pg_toast_9", "^pg_[a-z]+_[0-9]$", false));
2194 assert!(!regex_match("pg_toast_x", "^pg_[a-z]+_[0-9]$", false));
2195 assert!(regex_match("x", "^[^0-9]$", false));
2196 assert!(!regex_match("5", "^[^0-9]$", false));
2197 assert!(regex_match("a-b", "^a[-]b$", false));
2198 assert!(regex_match("a.b", "^a\\.b$", false));
2200 assert!(!regex_match("axb", "^a\\.b$", false));
2201 assert!(regex_match("(x)", "^\\(x\\)$", false));
2202 assert!(regex_match("q", "^()*q$", false));
2204 assert!(regex_match("héllo", "^h.l+o$", false));
2206 }
2207
2208 #[test]
2209 fn an_unsupported_regex_construct_is_REFUSED_BY_NAME_not_approximated() {
2210 for (pat, needle) in [
2214 ("a{2}", "interval"),
2215 ("[[:alpha:]]", "POSIX character class"),
2216 ("(a)\\1", "back-reference"),
2217 ("\\d+", "shorthand class"),
2218 ("(ab", "unmatched '('"),
2219 ("ab)", "unmatched ')'"),
2220 ("[ab", "unmatched '['"),
2221 ("*a", "nothing to repeat"),
2222 ("[z-a]", "reversed"),
2223 ] {
2224 let why = regex_error(pat).unwrap_or_else(|| {
2225 panic!("{:?} must be refused, not matched approximately", pat)
2226 });
2227 assert!(why.contains(needle), "{:?}: {:?} should name {:?}", pat, why, needle);
2228 assert!(!regex_match("aa", pat, false));
2230 }
2231 for pat in ["^pg_", "sql$", "^public$", "a.c", "plain", "", "^(orders)$",
2232 "a+b", "a*b", "a?b", "[ab]", "(a|b)", "a\\.b", "a[-]b"] {
2233 assert_eq!(regex_error(pat), None, "{:?} is in the subset", pat);
2234 }
2235 }
2236
2237 #[test]
2238 fn the_regex_operators_parse_in_all_four_spellings() {
2239 for (nql, negated, ci) in [
2240 (r#"FROM t WHERE nspname ~ "^pg_""#, false, false),
2241 (r#"FROM t WHERE nspname ~* "^pg_""#, false, true),
2242 (r#"FROM t WHERE nspname !~ "^pg_""#, true, false),
2243 (r#"FROM t WHERE nspname !~* "^pg_""#, true, true),
2244 ] {
2245 let q = parse(nql).unwrap_or_else(|e| panic!("{}: {}", nql, e));
2246 match q.where_.expect("a predicate") {
2247 Pred::Regex { field, pattern, negated: n, ci: c } => {
2248 assert_eq!(field, "nspname");
2249 assert_eq!(pattern, "^pg_");
2250 assert_eq!((n, c), (negated, ci), "{}", nql);
2251 }
2252 other => panic!("{} parsed as {:?}", nql, other),
2253 }
2254 }
2255 }
2256
2257 #[test]
2258 fn a_bad_regex_is_rejected_at_parse_time_with_the_offending_char() {
2259 let e = parse(r#"FROM t WHERE x ~ "a{2}""#).unwrap_err().to_string();
2260 assert!(e.contains("interval"), "the error must name the construct: {}", e);
2261 assert!(e.contains("refused"), "{}", e);
2262 }
2263
2264 #[test]
2265 fn a_regex_over_a_missing_field_is_false_in_both_polarities() {
2266 let (_tmp, db) = setup_items();
2269 let (m, _) = query(&db, r#"FROM items WHERE nosuchfield ~ "x""#).unwrap();
2270 assert!(m.is_empty());
2271 let (n, _) = query(&db, r#"FROM items WHERE nosuchfield !~ "x""#).unwrap();
2272 assert!(n.is_empty(), "NOT over NULL must not match either");
2273 }
2274
2275 #[test]
2276 fn the_regex_operator_works_end_to_end_over_stored_documents() {
2277 let (_tmp, db) = setup_items();
2278 let (all, _) = query(&db, "FROM items").unwrap();
2279 let want: Vec<String> = all.iter()
2280 .filter_map(|r| r["_id"].as_str().map(str::to_string)).collect();
2281 let (got, _) = query(&db, r#"FROM items WHERE _id ~ """#).unwrap();
2284 assert_eq!(got.len(), want.len());
2285 let (none, _) = query(&db, r#"FROM items WHERE _id !~ """#).unwrap();
2287 assert!(none.is_empty());
2288 }
2289
2290 #[test]
2291 fn where_not_like() {
2292 let (_tmp, db) = setup_text();
2293 let (rows, _) = query(&db, r#"FROM jobs WHERE status NOT LIKE "open""#).unwrap();
2294 assert_eq!(ids(&rows), vec!["2", "3", "5"]);
2295 }
2296
2297 #[test]
2301 fn like_over_null_is_false_in_both_polarities() {
2302 let (_tmp, db) = setup_text();
2303 let (pos, _) = query(&db, r#"FROM jobs WHERE miner LIKE "%""#).unwrap();
2304 let (neg, _) = query(&db, r#"FROM jobs WHERE miner NOT LIKE "%""#).unwrap();
2305 assert!(!ids(&pos).contains(&"4".to_string()));
2306 assert!(!ids(&neg).contains(&"4".to_string()));
2307 assert!(!ids(&pos).contains(&"5".to_string()));
2308 assert!(!ids(&neg).contains(&"5".to_string()));
2309 }
2310
2311 #[test]
2314 fn where_is_null_catches_absent_and_explicit_null() {
2315 let (_tmp, db) = setup_text();
2316 let (rows, _) = query(&db, "FROM jobs WHERE miner IS NULL").unwrap();
2317 assert_eq!(ids(&rows), vec!["4", "5"]);
2318 }
2319
2320 #[test]
2321 fn where_is_not_null() {
2322 let (_tmp, db) = setup_text();
2323 let (rows, _) = query(&db, "FROM jobs WHERE miner IS NOT NULL").unwrap();
2324 assert_eq!(ids(&rows), vec!["1", "2", "3"]);
2325 }
2326
2327 #[test]
2328 fn where_or() {
2329 let (_tmp, db) = setup();
2330 let (rows, _) = query(&db, "FROM blocks WHERE height = 1 OR height = 5").unwrap();
2331 assert_eq!(ids(&rows), vec!["1", "5"]);
2332 }
2333
2334 #[test]
2337 fn and_binds_tighter_than_or() {
2338 let (_tmp, db) = setup();
2339 let (rows, _) = query(
2340 &db, "FROM blocks WHERE height = 1 OR height = 5 AND n_tx = 10").unwrap();
2341 assert_eq!(ids(&rows), vec!["1", "5"]);
2342 let (rows2, _) = query(
2343 &db, "FROM blocks WHERE height = 1 OR height = 5 AND n_tx = 99").unwrap();
2344 assert_eq!(ids(&rows2), vec!["1"], "the AND arm must not match");
2345 }
2346
2347 #[test]
2349 fn parens_override_precedence() {
2350 let (_tmp, db) = setup();
2351 let (rows, _) = query(
2352 &db, "FROM blocks WHERE (height = 1 OR height = 5) AND n_tx = 10").unwrap();
2353 assert_eq!(ids(&rows), vec!["5"]);
2354 }
2355
2356 #[test]
2357 fn nested_parens() {
2358 let (_tmp, db) = setup();
2359 let (rows, _) = query(
2360 &db,
2361 "FROM blocks WHERE ((height >= 2 AND height <= 4) OR height = 1) AND n_tx != 6",
2362 ).unwrap();
2363 assert_eq!(ids(&rows), vec!["1", "2", "4"]);
2364 }
2365
2366 #[test]
2367 fn not_negates_a_group() {
2368 let (_tmp, db) = setup();
2369 let (rows, _) = query(&db, "FROM blocks WHERE NOT (height > 2)").unwrap();
2370 assert_eq!(ids(&rows), vec!["1", "2"]);
2371 }
2372
2373 #[test]
2377 fn prefix_not_before_a_comparison() {
2378 let (_tmp, db) = setup();
2379 let (rows, _) = query(&db, "FROM blocks WHERE NOT height = 1").unwrap();
2380 assert_eq!(ids(&rows), vec!["2", "3", "4", "5"]);
2381 let (double, _) = query(&db, "FROM blocks WHERE NOT NOT height = 1").unwrap();
2382 assert_eq!(ids(&double), vec!["1"]);
2383 let (mixed, _) = query(&db, "FROM blocks WHERE NOT height = 1 AND height < 4").unwrap();
2384 assert_eq!(ids(&mixed), vec!["2", "3"]);
2385 }
2386
2387 #[test]
2391 fn id_equality_under_or_does_not_become_a_point_lookup() {
2392 let (_tmp, db) = setup();
2393 let (rows, _) = query(&db, r#"FROM blocks WHERE _id = "1" OR height > 3"#).unwrap();
2394 assert_eq!(ids(&rows), vec!["1", "4", "5"],
2395 "the OR arm must survive the id fast path");
2396 }
2397
2398 #[test]
2400 fn id_equality_under_and_still_point_looks_up() {
2401 let (_tmp, db) = setup();
2402 let (hit, _) = query(&db, r#"FROM blocks WHERE _id = "3" AND n_tx = 6"#).unwrap();
2403 assert_eq!(ids(&hit), vec!["3"]);
2404 let (miss, _) = query(&db, r#"FROM blocks WHERE _id = "3" AND n_tx = 999"#).unwrap();
2405 assert!(miss.is_empty(), "the second conjunct must still be applied");
2406 }
2407
2408 #[test]
2409 fn metadata_fields_are_filterable() {
2410 let (_tmp, db) = setup();
2411 let (rows, _) = query(&db, "FROM blocks WHERE _seq >= 0 AND _coll = blocks").unwrap();
2412 assert_eq!(rows.len(), 5);
2413
2414 let first = rows.iter()
2420 .filter_map(|r| r.get("_seq").and_then(|v| v.as_u64()))
2421 .min()
2422 .expect("five rows carrying _seq");
2423 let (tail, _) = query(&db, &format!("FROM blocks WHERE _seq > {}", first)).unwrap();
2424 assert_eq!(tail.len(), 4);
2425 }
2426
2427 #[test]
2429 fn a_reserved_collection_never_leaks_into_a_user_query() {
2430 let (_tmp, db) = setup();
2431 let (rows, _) = query(&db, "FROM blocks").unwrap();
2432 assert!(
2433 rows.iter().all(|r| r.get("_coll").and_then(|v| v.as_str()) == Some("blocks")),
2434 "a query for one collection returned rows from another"
2435 );
2436 assert!(
2437 !db.collections().iter().any(|c| crate::namespace::is_reserved(c)),
2438 "the registry is not a user collection"
2439 );
2440 }
2441
2442 #[test]
2443 fn combined_with_order_and_limit() {
2444 let (_tmp, db) = setup();
2445 let (rows, _) = query(
2446 &db,
2447 "FROM blocks WHERE height IN (1, 3, 5) ORDER BY height DESC LIMIT 2",
2448 ).unwrap();
2449 let heights: Vec<u64> = rows.iter().filter_map(|r| r["height"].as_u64()).collect();
2450 assert_eq!(heights, vec![5, 3]);
2451 }
2452
2453 #[test]
2459 fn unknown_clauses_are_errors_not_silent_skips() {
2460 let (_tmp, db) = setup();
2461 for bad in [
2462 "FROM blocks ORDRE BY height", "FROM blocks WHERE height > 3 JUNK", "FROM blocks SELECT height", "FROM blocks LIMIT", "FROM blocks OFFSET", "FROM blocks ORDER BY", "FROM blocks ORDER BY height,", ] {
2470 assert!(query(&db, bad).is_err(), "`{}` must be rejected, not silently reinterpreted", bad);
2471 }
2472 }
2473
2474 #[test]
2475 fn malformed_predicates_are_errors() {
2476 let (_tmp, db) = setup();
2477 for bad in [
2478 "FROM blocks WHERE height >", "FROM blocks WHERE height IN (", "FROM blocks WHERE height IN ()", "FROM blocks WHERE height BETWEEN 1", "FROM blocks WHERE height BETWEEN 1 3", "FROM blocks WHERE (height = 1", "FROM blocks WHERE height IS 3", "FROM blocks WHERE height NOT = 1", "FROM blocks WHERE height LIKE", ] {
2488 assert!(query(&db, bad).is_err(), "`{}` must be a parse error", bad);
2489 }
2490 }
2491
2492 #[test]
2496 fn asc_is_accepted_explicitly() {
2497 let (_tmp, db) = setup();
2498 let (asc, _) = query(&db, "FROM blocks ORDER BY height ASC").unwrap();
2499 let (plain, _) = query(&db, "FROM blocks ORDER BY height").unwrap();
2500 assert_eq!(asc[0]["height"], 1);
2501 assert_eq!(plain[0]["height"], 1);
2502 }
2503
2504 #[test]
2507 fn new_keywords_are_case_insensitive() {
2508 let (_tmp, db) = setup();
2509 let (rows, _) = query(&db, "from blocks where height between 2 and 3").unwrap();
2510 assert_eq!(ids(&rows), vec!["2", "3"]);
2511 let (rows2, _) = query(&db, "FROM blocks Where height In (1) Or height In (2)").unwrap();
2512 assert_eq!(ids(&rows2), vec!["1", "2"]);
2513 }
2514
2515 #[test]
2516 fn group_by_count() {
2517 let (_tmp, db) = setup();
2518 let (rows, _) = query(&db, "FROM blocks GROUP BY n_tx COUNT").unwrap();
2519 assert_eq!(rows.len(), 5); }
2521
2522 fn twin(fields: &[&str]) -> (tempfile::TempDir, tempfile::TempDir, Db, Db) {
2534 let d1 = tempdir().unwrap();
2535 let d2 = tempdir().unwrap();
2536 let indexed = Db::open(d1.path(), None).unwrap();
2537 let plain = Db::open(d2.path(), None).unwrap();
2538 for f in fields {
2539 indexed.create_sorted_index("t", f);
2540 }
2541 let rows: Vec<(String, Value)> = (0..40u64).map(|i| {
2544 let mut o = serde_json::Map::new();
2545 if i % 7 != 0 {
2546 o.insert("fee".into(), json!(i % 13));
2547 }
2548 if i % 11 == 0 {
2549 o.insert("note".into(), Value::Null);
2550 } else {
2551 o.insert("note".into(), json!(format!("n{}", i % 5)));
2552 }
2553 o.insert("rank".into(), json!(40 - i));
2554 (i.to_string(), Value::Object(o))
2555 }).collect();
2556 for (id, doc) in &rows {
2557 indexed.put("t", id, doc.clone(), vec![], None, None).unwrap();
2558 plain.put("t", id, doc.clone(), vec![], None, None).unwrap();
2559 }
2560 (d1, d2, indexed, plain)
2561 }
2562
2563 fn same(a: &Db, b: &Db, nql: &str) -> (Vec<String>, Vec<String>) {
2564 let ga = {
2565 let (rows, _) = query(a, nql).unwrap();
2566 let mut v: Vec<String> = rows.iter()
2567 .filter_map(|r| r["_id"].as_str().map(String::from)).collect();
2568 v.sort(); v
2569 };
2570 let gb = {
2571 let (rows, _) = query(b, nql).unwrap();
2572 let mut v: Vec<String> = rows.iter()
2573 .filter_map(|r| r["_id"].as_str().map(String::from)).collect();
2574 v.sort(); v
2575 };
2576 (ga, gb)
2577 }
2578
2579 #[test]
2580 fn indexed_and_unindexed_agree_on_every_predicate_shape() {
2581 let (_t1, _t2, idx, plain) = twin(&["fee", "note", "rank"]);
2582 for nql in [
2583 "FROM t WHERE fee > 5",
2585 "FROM t WHERE fee >= 5",
2586 "FROM t WHERE fee < 5",
2587 "FROM t WHERE fee <= 5",
2588 "FROM t WHERE fee = 5",
2589 "FROM t WHERE fee BETWEEN 3 AND 8",
2590 "FROM t WHERE fee NOT BETWEEN 3 AND 8",
2591 "FROM t WHERE fee IN (1, 5, 9)",
2592 "FROM t WHERE fee NOT IN (1, 5, 9)",
2593 "FROM t WHERE fee != 5",
2594 "FROM t WHERE fee > 3 AND fee < 9",
2596 "FROM t WHERE fee >= 3 AND fee <= 9",
2597 "FROM t WHERE fee > 3 AND fee < 9 AND fee != 5",
2598 "FROM t WHERE fee BETWEEN 2 AND 10 AND fee > 6",
2599 "FROM t WHERE fee > 5 AND rank < 20",
2601 "FROM t WHERE fee IN (2, 3) AND rank > 10",
2602 "FROM t WHERE fee = 4 AND rank = 8",
2603 "FROM t WHERE fee IS NULL",
2605 "FROM t WHERE fee IS NOT NULL",
2606 "FROM t WHERE note IS NULL",
2607 "FROM t WHERE note IS NOT NULL",
2608 "FROM t WHERE fee IS NULL AND rank > 20",
2609 r#"FROM t WHERE note LIKE "n_""#,
2611 r#"FROM t WHERE fee > 5 AND note LIKE "n1""#,
2612 r#"FROM t WHERE note NOT LIKE "n1" AND fee < 4"#,
2613 "FROM t WHERE fee > 11 OR rank > 38",
2615 "FROM t WHERE fee = 1 OR note IS NULL",
2616 "FROM t WHERE (fee > 11 OR rank > 38) AND rank < 39",
2617 "FROM t WHERE fee IN (1) OR fee IN (2)",
2618 "FROM t WHERE NOT (fee > 5)",
2620 "FROM t WHERE NOT (fee IN (1, 2))",
2621 "FROM t WHERE NOT (fee > 5) AND rank < 30",
2622 "FROM t WHERE fee > 5 ORDER BY rank DESC LIMIT 5",
2624 "FROM t WHERE fee BETWEEN 2 AND 8 ORDER BY fee, rank DESC",
2625 "FROM t WHERE fee > 5 GROUP BY note COUNT",
2626 "FROM t WHERE fee > 5 COUNT",
2627 "FROM t WHERE fee > 5 ORDER BY rank LIMIT 3 OFFSET 2",
2628 "FROM t WHERE fee > 9999",
2630 "FROM t WHERE fee IN (9999)",
2631 "FROM t WHERE fee BETWEEN 100 AND 200",
2632 ] {
2633 let (a, b) = same(&idx, &plain, nql);
2634 assert_eq!(a, b, "indexed and unindexed disagree on `{}`", nql);
2635 }
2636 }
2637
2638 #[test]
2642 fn index_path_still_honours_order_by() {
2643 let (_t1, _t2, idx, plain) = twin(&["fee", "rank"]);
2644 for nql in [
2645 "FROM t WHERE fee > 4 ORDER BY rank",
2646 "FROM t WHERE fee > 4 ORDER BY rank DESC",
2647 "FROM t WHERE fee > 4 ORDER BY note, rank DESC",
2648 "FROM t WHERE fee BETWEEN 2 AND 9 ORDER BY rank LIMIT 4",
2649 "FROM t WHERE fee IN (3, 6) ORDER BY rank DESC LIMIT 2",
2650 ] {
2651 let ra = query(&idx, nql).unwrap().0;
2652 let rb = query(&plain, nql).unwrap().0;
2653 let ia: Vec<&str> = ra.iter().filter_map(|r| r["_id"].as_str()).collect();
2654 let ib: Vec<&str> = rb.iter().filter_map(|r| r["_id"].as_str()).collect();
2655 assert_eq!(ia, ib, "row ORDER differs on `{}`", nql);
2656 }
2657 }
2658
2659 #[test]
2668 fn an_ordering_comparison_against_a_missing_field_is_false() {
2669 let dir = tempdir().unwrap();
2670 let db = Db::open(dir.path(), None).unwrap();
2671 db.put("t", "has", json!({"fee": 1}), vec![], None, None).unwrap();
2672 db.put("t", "none", json!({"other": 1}), vec![], None, None).unwrap();
2673 db.put("t", "null", json!({"fee": Value::Null}), vec![], None, None).unwrap();
2674
2675 for nql in ["FROM t WHERE fee < 5", "FROM t WHERE fee <= 5"] {
2676 let (r, _) = query(&db, nql).unwrap();
2677 let ids: Vec<&str> = r.iter().filter_map(|x| x["_id"].as_str()).collect();
2678 assert_eq!(ids, vec!["has"],
2679 "`{}` must not match a row whose fee is absent or null", nql);
2680 }
2681 for nql in ["FROM t WHERE fee > 0", "FROM t WHERE fee >= 0"] {
2682 let (r, _) = query(&db, nql).unwrap();
2683 let ids: Vec<&str> = r.iter().filter_map(|x| x["_id"].as_str()).collect();
2684 assert_eq!(ids, vec!["has"], "`{}`", nql);
2685 }
2686 let (b, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 9").unwrap();
2688 assert_eq!(b.len(), 1);
2689
2690 let (ne, _) = query(&db, "FROM t WHERE fee != 5").unwrap();
2693 assert_eq!(ne.len(), 3, "!= still matches absent and null fields");
2694 let (isnull, _) = query(&db, "FROM t WHERE fee = NULL").unwrap();
2695 assert_eq!(isnull.len(), 2, "absent and explicit-null both equal NULL");
2696
2697 let d2 = tempdir().unwrap();
2700 let idx = Db::open(d2.path(), None).unwrap();
2701 idx.create_sorted_index("t", "fee");
2702 idx.put("t", "has", json!({"fee": 1}), vec![], None, None).unwrap();
2703 idx.put("t", "none", json!({"other": 1}), vec![], None, None).unwrap();
2704 idx.put("t", "null", json!({"fee": Value::Null}), vec![], None, None).unwrap();
2705 for nql in ["FROM t WHERE fee < 5", "FROM t WHERE fee <= 5",
2706 "FROM t WHERE fee > 0", "FROM t WHERE fee BETWEEN 0 AND 9"] {
2707 let (a, _) = query(&db, nql).unwrap();
2708 let (b, _) = query(&idx, nql).unwrap();
2709 let ia: Vec<&str> = a.iter().filter_map(|x| x["_id"].as_str()).collect();
2710 let ib: Vec<&str> = b.iter().filter_map(|x| x["_id"].as_str()).collect();
2711 assert_eq!(ia, ib, "indexed and unindexed disagree on `{}`", nql);
2712 }
2713 }
2714
2715 #[test]
2720 fn is_null_never_uses_the_index() {
2721 let (_t1, _t2, idx, plain) = twin(&["fee"]);
2722 let (a, b) = same(&idx, &plain, "FROM t WHERE fee IS NULL");
2723 assert_eq!(a, b);
2724 assert_eq!(a, vec!["0", "14", "21", "28", "35", "7"]);
2726 assert!(!a.is_empty(), "the fixture must actually contain absent fields");
2727 }
2728
2729 #[test]
2732 fn a_disjunct_is_never_used_to_narrow() {
2733 let (_t1, _t2, idx, plain) = twin(&["fee", "rank"]);
2734 let nql = "FROM t WHERE fee = 1 OR rank = 40";
2735 let (a, b) = same(&idx, &plain, nql);
2736 assert_eq!(a, b);
2737 assert!(a.contains(&"0".to_string()),
2740 "the OR arm matching a doc with no indexed field must survive: {:?}", a);
2741 assert!(a.len() > 1, "both arms must contribute: {:?}", a);
2742 }
2743
2744 #[test]
2748 fn as_of_does_not_use_the_current_version_index() {
2749 let dir = tempdir().unwrap();
2750 let db = Db::open(dir.path(), None).unwrap();
2751 db.create_sorted_index("t", "fee");
2752 db.put("t", "a", json!({"fee": 5}), vec![], None, None).unwrap();
2753 let snap = db.put("t", "b", json!({"fee": 5}), vec![], None, None).unwrap().seq;
2754 db.put("t", "a", json!({"fee": 999}), vec![], None, None).unwrap();
2756 db.put("t", "b", json!({"fee": 999}), vec![], None, None).unwrap();
2757
2758 let (now, _) = query(&db, "FROM t WHERE fee = 5").unwrap();
2760 assert!(now.is_empty(), "current versions have fee 999: {:?}", now);
2761
2762 let (then, _) = query(&db, &format!("FROM t AS OF {} WHERE fee = 5", snap)).unwrap();
2765 let mut ids: Vec<&str> = then.iter().filter_map(|r| r["_id"].as_str()).collect();
2766 ids.sort();
2767 assert_eq!(ids, vec!["a", "b"], "AS OF must see the historical values");
2768
2769 let (r, _) = query(&db, &format!("FROM t AS OF {} WHERE fee BETWEEN 1 AND 9", snap)).unwrap();
2771 assert_eq!(r.len(), 2);
2772 let (i, _) = query(&db, &format!("FROM t AS OF {} WHERE fee IN (5)", snap)).unwrap();
2773 assert_eq!(i.len(), 2);
2774 }
2775
2776 #[test]
2778 fn the_index_path_returns_current_versions_only() {
2779 let dir = tempdir().unwrap();
2780 let db = Db::open(dir.path(), None).unwrap();
2781 db.create_sorted_index("t", "fee");
2782 for i in 0..5u64 {
2783 db.put("t", &i.to_string(), json!({"fee": i}), vec![], None, None).unwrap();
2784 }
2785 db.put("t", "0", json!({"fee": 100}), vec![], None, None).unwrap();
2786
2787 let (low, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 4").unwrap();
2788 let mut ids: Vec<&str> = low.iter().filter_map(|r| r["_id"].as_str()).collect();
2789 ids.sort();
2790 assert_eq!(ids, vec!["1", "2", "3", "4"],
2791 "doc 0 moved to fee 100 and must not appear in 0..4");
2792
2793 let (high, _) = query(&db, "FROM t WHERE fee = 100").unwrap();
2794 assert_eq!(high.len(), 1);
2795 assert_eq!(high[0]["_id"], "0");
2796 assert_eq!(high[0]["fee"], json!(100), "the CURRENT value, not the old one");
2797 }
2798
2799 #[test]
2802 fn index_scans_do_not_duplicate_rows() {
2803 let dir = tempdir().unwrap();
2804 let db = Db::open(dir.path(), None).unwrap();
2805 db.create_sorted_index("t", "fee");
2806 for i in 0..6u64 {
2807 db.put("t", &i.to_string(), json!({"fee": i % 2}), vec![], None, None).unwrap();
2808 }
2809 let (dup, _) = query(&db, "FROM t WHERE fee IN (0, 0, 1, 1)").unwrap();
2810 assert_eq!(dup.len(), 6, "each row once despite repeated IN arms");
2811 let (r, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 1").unwrap();
2812 assert_eq!(r.len(), 6);
2813 let mut ids: Vec<&str> = dup.iter().filter_map(|r| r["_id"].as_str()).collect();
2814 ids.sort();
2815 ids.dedup();
2816 assert_eq!(ids.len(), 6, "no duplicate _ids");
2817 }
2818
2819 #[test]
2821 fn index_ranges_work_on_strings() {
2822 let dir = tempdir().unwrap();
2823 let db = Db::open(dir.path(), None).unwrap();
2824 db.create_sorted_index("t", "name");
2825 for (i, n) in ["alpha", "bravo", "charlie", "delta", "echo"].iter().enumerate() {
2826 db.put("t", &i.to_string(), json!({"name": n}), vec![], None, None).unwrap();
2827 }
2828 let (r, _) = query(&db, r#"FROM t WHERE name BETWEEN "bravo" AND "delta""#).unwrap();
2829 let mut got: Vec<&str> = r.iter().filter_map(|x| x["name"].as_str()).collect();
2830 got.sort();
2831 assert_eq!(got, vec!["bravo", "charlie", "delta"]);
2832 let (gt, _) = query(&db, r#"FROM t WHERE name > "charlie""#).unwrap();
2833 assert_eq!(gt.len(), 2);
2834 }
2835
2836 #[test]
2839 fn same_field_bounds_are_merged_tightest_wins() {
2840 let (_t1, _t2, idx, plain) = twin(&["fee"]);
2841 for (a_nql, b_nql) in [
2842 ("FROM t WHERE fee > 2 AND fee > 6", "FROM t WHERE fee > 6"),
2843 ("FROM t WHERE fee > 6 AND fee > 2", "FROM t WHERE fee > 6"),
2844 ("FROM t WHERE fee < 9 AND fee < 4", "FROM t WHERE fee < 4"),
2845 ("FROM t WHERE fee BETWEEN 0 AND 12 AND fee >= 5 AND fee <= 7",
2846 "FROM t WHERE fee >= 5 AND fee <= 7"),
2847 ] {
2848 let (ia, _) = same(&idx, &plain, a_nql);
2849 let (ib, _) = same(&idx, &plain, b_nql);
2850 assert_eq!(ia, ib, "`{}` should equal `{}`", a_nql, b_nql);
2851 }
2852 }
2853
2854 #[test]
2857 fn a_predicate_on_an_unindexed_field_still_answers() {
2858 let (_t1, _t2, idx, plain) = twin(&["fee"]); for nql in [
2860 "FROM t WHERE rank > 30",
2861 "FROM t WHERE rank BETWEEN 10 AND 20",
2862 "FROM t WHERE rank IN (40, 39)",
2863 "FROM t WHERE rank > 30 AND fee > 2",
2864 ] {
2865 let (a, b) = same(&idx, &plain, nql);
2866 assert_eq!(a, b, "`{}`", nql);
2867 }
2868 }
2869
2870 #[test]
2873 fn range_cardinality_counts_without_reading() {
2874 let dir = tempdir().unwrap();
2875 let db = Db::open(dir.path(), None).unwrap();
2876 db.create_sorted_index("t", "fee");
2877 for i in 0..20u64 {
2878 db.put("t", &i.to_string(), json!({"fee": i}), vec![], None, None).unwrap();
2879 }
2880 assert_eq!(db.range_cardinality("t", "fee", None, None, true, true), Some(20));
2881 assert_eq!(
2882 db.range_cardinality("t", "fee", Some(&json!(5)), Some(&json!(9)), true, true),
2883 Some(5), "5..=9 inclusive is five values");
2884 assert_eq!(
2885 db.range_cardinality("t", "fee", Some(&json!(5)), Some(&json!(9)), false, false),
2886 Some(3), "exclusive bounds drop both ends");
2887 assert_eq!(
2888 db.range_cardinality("t", "fee", Some(&json!(18)), None, true, true),
2889 Some(2));
2890 assert_eq!(
2891 db.range_cardinality("t", "fee", Some(&json!(999)), None, true, true),
2892 Some(0), "an empty range is 0, not an error");
2893 assert_eq!(db.range_cardinality("t", "nope", None, None, true, true), None);
2895 }
2896
2897 #[test]
2901 fn the_narrower_index_is_preferred() {
2902 let dir = tempdir().unwrap();
2903 let db = Db::open(dir.path(), None).unwrap();
2904 db.create_sorted_index("t", "wide");
2905 db.create_sorted_index("t", "narrow");
2906 for i in 0..100u64 {
2907 db.put("t", &i.to_string(),
2908 json!({"wide": i % 2, "narrow": i}), vec![], None, None).unwrap();
2909 }
2910 let wide = db.range_cardinality("t", "wide", Some(&json!(0)), Some(&json!(0)), true, true);
2912 let narrow = db.range_cardinality("t", "narrow", Some(&json!(7)), Some(&json!(7)), true, true);
2913 assert_eq!(wide, Some(50));
2914 assert_eq!(narrow, Some(1));
2915 let (r, _) = query(&db, "FROM t WHERE wide = 0 AND narrow = 7").unwrap();
2917 assert!(r.is_empty(), "narrow 7 has wide 1, so nothing matches");
2918 let (r2, _) = query(&db, "FROM t WHERE wide = 0 AND narrow = 8").unwrap();
2919 assert_eq!(r2.len(), 1);
2920 assert_eq!(r2[0]["_id"], "8");
2921 }
2922
2923 fn heights(rows: &[Value]) -> Vec<u64> {
2927 rows.iter().filter_map(|r| r["height"].as_u64()).collect()
2928 }
2929
2930 #[test]
2931 fn offset_skips_result_rows() {
2932 let (_tmp, db) = setup();
2933 let (rows, _) = query(&db, "FROM blocks ORDER BY height OFFSET 2").unwrap();
2934 assert_eq!(heights(&rows), vec![3, 4, 5]);
2935 }
2936
2937 #[test]
2938 fn offset_with_limit_pages() {
2939 let (_tmp, db) = setup();
2940 let mut seen = vec![];
2944 for page in 0..3 {
2945 let (rows, _) = query(
2946 &db,
2947 &format!("FROM blocks ORDER BY height LIMIT 2 OFFSET {}", page * 2),
2948 ).unwrap();
2949 seen.extend(heights(&rows));
2950 }
2951 assert_eq!(seen, vec![1, 2, 3, 4, 5]);
2952 }
2953
2954 #[test]
2955 fn offset_past_the_end_is_an_empty_page() {
2956 let (_tmp, db) = setup();
2957 let (rows, count) = query(&db, "FROM blocks OFFSET 99").unwrap();
2958 assert!(rows.is_empty());
2959 assert_eq!(count, 0);
2960 let (zero, _) = query(&db, "FROM blocks OFFSET 0").unwrap();
2961 assert_eq!(zero.len(), 5, "OFFSET 0 skips nothing");
2962 }
2963
2964 #[test]
2965 fn offset_applies_after_the_filter() {
2966 let (_tmp, db) = setup();
2967 let (rows, _) = query(
2970 &db, "FROM blocks WHERE n_tx >= 6 ORDER BY height OFFSET 1").unwrap();
2971 assert_eq!(heights(&rows), vec![4, 5]);
2972 }
2973
2974 #[test]
2975 fn order_by_multiple_keys() {
2976 let dir = tempdir().unwrap();
2977 let db = Db::open(dir.path(), None).unwrap();
2978 for (i, (s, f)) in [("open", 30), ("open", 10), ("closed", 20),
2981 ("open", 20), ("closed", 5)].iter().enumerate() {
2982 db.put("t", &i.to_string(),
2983 serde_json::json!({"status": s, "fee": f}), vec![], None, None).unwrap();
2984 }
2985 let (rows, _) = query(&db, "FROM t ORDER BY status, fee DESC").unwrap();
2986 let got: Vec<(String, u64)> = rows.iter()
2987 .map(|r| (r["status"].as_str().unwrap().to_string(), r["fee"].as_u64().unwrap()))
2988 .collect();
2989 assert_eq!(got, vec![
2990 ("closed".into(), 20), ("closed".into(), 5),
2991 ("open".into(), 30), ("open".into(), 20), ("open".into(), 10),
2992 ]);
2993 }
2994
2995 #[test]
2996 fn order_by_mixed_directions() {
2997 let dir = tempdir().unwrap();
2998 let db = Db::open(dir.path(), None).unwrap();
2999 for (i, (a, b)) in [(1, 1), (1, 2), (2, 1), (2, 2)].iter().enumerate() {
3000 db.put("t", &i.to_string(),
3001 serde_json::json!({"a": a, "b": b}), vec![], None, None).unwrap();
3002 }
3003 let (rows, _) = query(&db, "FROM t ORDER BY a DESC, b ASC").unwrap();
3004 let got: Vec<(u64, u64)> = rows.iter()
3005 .map(|r| (r["a"].as_u64().unwrap(), r["b"].as_u64().unwrap()))
3006 .collect();
3007 assert_eq!(got, vec![(2, 1), (2, 2), (1, 1), (1, 2)]);
3008 }
3009
3010 #[test]
3015 fn limit_applies_to_grouped_rows_not_to_the_input() {
3016 let dir = tempdir().unwrap();
3017 let db = Db::open(dir.path(), None).unwrap();
3018 for i in 0..12 {
3019 let status = ["open", "closed", "void"][i % 3];
3021 db.put("t", &i.to_string(),
3022 serde_json::json!({"status": status, "fee": i}),
3023 vec![], None, None).unwrap();
3024 }
3025 let (all, _) = query(&db, "FROM t GROUP BY status COUNT").unwrap();
3026 assert_eq!(all.len(), 3);
3027 let total: u64 = all.iter().filter_map(|r| r["count"].as_u64()).sum();
3028 assert_eq!(total, 12, "every input row must be counted");
3029
3030 let (limited, _) = query(&db, "FROM t GROUP BY status COUNT LIMIT 2").unwrap();
3033 assert_eq!(limited.len(), 2, "LIMIT caps the number of groups");
3034 for r in &limited {
3035 assert_eq!(r["count"], json!(4),
3036 "each group keeps its true count, got {:?}", r);
3037 }
3038 }
3039
3040 #[test]
3045 fn order_by_sorts_the_grouped_rows() {
3046 let dir = tempdir().unwrap();
3047 let db = Db::open(dir.path(), None).unwrap();
3048 for (i, s) in ["a", "b", "b", "b", "c", "c"].iter().enumerate() {
3050 db.put("t", &i.to_string(),
3051 serde_json::json!({"g": s, "n": i}), vec![], None, None).unwrap();
3052 }
3053 let (rows, _) = query(&db, "FROM t GROUP BY g COUNT ORDER BY count DESC").unwrap();
3054 let got: Vec<(String, u64)> = rows.iter()
3055 .map(|r| (r["g"].as_str().unwrap().to_string(), r["count"].as_u64().unwrap()))
3056 .collect();
3057 assert_eq!(got, vec![("b".into(), 3), ("c".into(), 2), ("a".into(), 1)]);
3058
3059 let (by_key, _) = query(&db, "FROM t GROUP BY g COUNT ORDER BY g DESC").unwrap();
3061 let keys: Vec<&str> = by_key.iter().map(|r| r["g"].as_str().unwrap()).collect();
3062 assert_eq!(keys, vec!["c", "b", "a"]);
3063 }
3064
3065 #[test]
3066 fn order_by_an_aggregate_key() {
3067 let (_tmp, db) = setup_items();
3068 let (rows, _) = query(
3069 &db, "FROM items GROUP BY cat SUM price ORDER BY sum_price DESC").unwrap();
3070 let cats: Vec<&str> = rows.iter().map(|r| r["cat"].as_str().unwrap()).collect();
3071 assert_eq!(cats, vec!["y", "x"], "y sums to 60, x to 15");
3072 }
3073
3074 #[test]
3075 fn offset_and_limit_page_grouped_rows() {
3076 let dir = tempdir().unwrap();
3077 let db = Db::open(dir.path(), None).unwrap();
3078 for i in 0..9 {
3079 db.put("t", &i.to_string(),
3080 serde_json::json!({"g": format!("g{}", i % 3)}), vec![], None, None).unwrap();
3081 }
3082 let (page, _) = query(
3083 &db, "FROM t GROUP BY g COUNT ORDER BY g LIMIT 1 OFFSET 1").unwrap();
3084 assert_eq!(page.len(), 1);
3085 assert_eq!(page[0]["g"], "g1");
3086 }
3087
3088 #[test]
3091 fn having_filters_groups_by_count() {
3092 let dir = tempdir().unwrap();
3093 let db = Db::open(dir.path(), None).unwrap();
3094 for (i, s) in ["a", "b", "b", "b", "c", "c"].iter().enumerate() {
3095 db.put("t", &i.to_string(),
3096 serde_json::json!({"g": s, "n": i}), vec![], None, None).unwrap();
3097 }
3098 let (rows, _) = query(&db, "FROM t GROUP BY g COUNT HAVING count > 1").unwrap();
3099 let mut keys: Vec<&str> = rows.iter().map(|r| r["g"].as_str().unwrap()).collect();
3100 keys.sort();
3101 assert_eq!(keys, vec!["b", "c"], "the single-row group `a` is filtered out");
3102 }
3103
3104 #[test]
3105 fn having_filters_on_the_aggregate_value() {
3106 let (_tmp, db) = setup_items();
3107 let (rows, _) = query(
3109 &db, "FROM items GROUP BY cat SUM price HAVING sum_price > 20").unwrap();
3110 assert_eq!(rows.len(), 1);
3111 assert_eq!(rows[0]["cat"], "y");
3112 }
3113
3114 #[test]
3117 fn having_supports_the_whole_predicate_surface() {
3118 let (_tmp, db) = setup_items();
3119 let (in_, _) = query(
3120 &db, r#"FROM items GROUP BY cat COUNT HAVING cat IN ("x")"#).unwrap();
3121 assert_eq!(in_.len(), 1);
3122 assert_eq!(in_[0]["cat"], "x");
3123
3124 let (btw, _) = query(
3125 &db, "FROM items GROUP BY cat SUM price HAVING sum_price BETWEEN 10 AND 20").unwrap();
3126 assert_eq!(btw.len(), 1);
3127 assert_eq!(btw[0]["cat"], "x");
3128
3129 let (like, _) = query(
3130 &db, r#"FROM items GROUP BY cat COUNT HAVING cat LIKE "y""#).unwrap();
3131 assert_eq!(like.len(), 1);
3132
3133 let (or_, _) = query(
3134 &db, "FROM items GROUP BY cat SUM price HAVING sum_price < 20 OR count = 3").unwrap();
3135 assert_eq!(or_.len(), 2);
3136 }
3137
3138 #[test]
3141 fn where_and_having_are_different_stages() {
3142 let (_tmp, db) = setup_items();
3143 let (w, _) = query(
3145 &db, "FROM items WHERE price > 10 GROUP BY cat SUM price").unwrap();
3146 let x = w.iter().find(|r| r["cat"] == "x");
3147 assert!(x.is_none(), "x's rows (0,5,10) are all filtered out by WHERE");
3148
3149 let (h, _) = query(
3151 &db, "FROM items GROUP BY cat SUM price HAVING sum_price > 10").unwrap();
3152 assert_eq!(h.len(), 2, "both groups sum above 10 when nothing is pre-filtered");
3153 }
3154
3155 #[test]
3156 fn having_without_an_aggregate_is_an_error() {
3157 let (_tmp, db) = setup();
3158 assert!(query(&db, "FROM blocks HAVING height > 3").is_err());
3162 }
3163
3164 #[test]
3170 fn bare_count_returns_one_row() {
3171 let (_tmp, db) = setup();
3172 let (rows, _) = query(&db, "FROM blocks COUNT").unwrap();
3173 assert_eq!(rows.len(), 1);
3174 assert_eq!(rows[0]["count"], json!(5));
3175 assert_eq!(rows[0]["value"], json!(5));
3176 }
3177
3178 #[test]
3179 fn bare_count_respects_the_filter() {
3180 let (_tmp, db) = setup();
3181 let (rows, _) = query(&db, "FROM blocks WHERE height > 3 COUNT").unwrap();
3182 assert_eq!(rows[0]["count"], json!(2));
3183 }
3184
3185 #[test]
3186 fn bare_sum_avg_min_max() {
3187 let (_tmp, db) = setup();
3188 let (s, _) = query(&db, "FROM blocks SUM n_tx").unwrap();
3190 assert_eq!(s[0]["sum_n_tx"], json!(30), "integer inputs give an integer sum");
3191 let (a, _) = query(&db, "FROM blocks AVG n_tx").unwrap();
3192 assert_eq!(a[0]["avg_n_tx"], json!(6.0));
3193 let (mn, _) = query(&db, "FROM blocks MIN n_tx").unwrap();
3194 assert_eq!(mn[0]["min_n_tx"], json!(2));
3195 let (mx, _) = query(&db, "FROM blocks MAX n_tx").unwrap();
3196 assert_eq!(mx[0]["max_n_tx"], json!(10));
3197 }
3198
3199 #[test]
3207 fn integer_aggregates_stay_integers_and_keep_full_precision() {
3208 let dir = tempdir().unwrap();
3209 let db = Db::open(dir.path(), None).unwrap();
3210 let big: [i64; 3] = [9_007_199_254_740_993, 9_007_199_254_740_995, 1];
3213 for (i, v) in big.iter().enumerate() {
3214 db.put("t", &i.to_string(), serde_json::json!({"v": v}),
3215 vec![], None, None).unwrap();
3216 }
3217 let (s, _) = query(&db, "FROM t SUM v").unwrap();
3218 assert_eq!(s[0]["sum_v"], json!(18_014_398_509_481_989i64),
3219 "exact i64 sum, not a rounded f64");
3220 assert!(s[0]["sum_v"].is_i64(), "must serialise as an integer");
3221
3222 let (mx, _) = query(&db, "FROM t MAX v").unwrap();
3223 assert_eq!(mx[0]["max_v"], json!(9_007_199_254_740_995i64));
3224 let (mn, _) = query(&db, "FROM t MIN v").unwrap();
3225 assert_eq!(mn[0]["min_v"], json!(1));
3226 }
3227
3228 #[test]
3231 fn a_single_float_makes_the_aggregate_fractional() {
3232 let dir = tempdir().unwrap();
3233 let db = Db::open(dir.path(), None).unwrap();
3234 db.put("t", "1", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
3235 db.put("t", "2", serde_json::json!({"v": 2.5}), vec![], None, None).unwrap();
3236 let (s, _) = query(&db, "FROM t SUM v").unwrap();
3237 assert_eq!(s[0]["sum_v"], json!(3.5));
3238 let (a, _) = query(&db, "FROM t AVG v").unwrap();
3240 assert_eq!(a[0]["avg_v"], json!(1.75));
3241 }
3242
3243 #[test]
3246 fn booleans_are_not_aggregated_as_numbers() {
3247 let dir = tempdir().unwrap();
3248 let db = Db::open(dir.path(), None).unwrap();
3249 db.put("t", "1", serde_json::json!({"v": true}), vec![], None, None).unwrap();
3250 db.put("t", "2", serde_json::json!({"v": 5}), vec![], None, None).unwrap();
3251 let (s, _) = query(&db, "FROM t SUM v").unwrap();
3252 assert_eq!(s[0]["sum_v"], json!(5), "the bool contributes nothing");
3253 assert_eq!(s[0]["count"], json!(2), "but it still counts toward the group");
3254 }
3255
3256 #[test]
3259 fn bare_count_of_nothing_is_zero_not_empty() {
3260 let (_tmp, db) = setup();
3261 let (rows, count) = query(&db, "FROM blocks WHERE height > 999 COUNT").unwrap();
3262 assert_eq!(count, 1, "still exactly one row");
3263 assert_eq!(rows[0]["count"], json!(0));
3264
3265 let (g, _) = query(&db, "FROM blocks WHERE height > 999 GROUP BY height COUNT").unwrap();
3267 assert!(g.is_empty());
3268 }
3269
3270 #[test]
3271 fn bare_aggregate_over_an_empty_collection() {
3272 let (_tmp, db) = setup();
3273 let (rows, _) = query(&db, "FROM nonexistent COUNT").unwrap();
3274 assert_eq!(rows[0]["count"], json!(0));
3275 let (s, _) = query(&db, "FROM nonexistent SUM n_tx").unwrap();
3276 assert_eq!(s[0]["sum_n_tx"], Value::Null, "sum of nothing is null, not 0");
3277 }
3278
3279 #[test]
3280 fn bare_aggregate_carries_no_group_key() {
3281 let (_tmp, db) = setup();
3282 let (rows, _) = query(&db, "FROM blocks COUNT").unwrap();
3283 if let Value::Object(m) = &rows[0] {
3284 let mut keys: Vec<&String> = m.keys().collect();
3285 keys.sort();
3286 assert_eq!(keys, vec!["count", "value"]);
3287 } else {
3288 panic!("expected an object");
3289 }
3290 }
3291
3292 #[test]
3299 fn a_field_named_like_a_keyword_is_still_addressable() {
3300 let dir = tempdir().unwrap();
3301 let db = Db::open(dir.path(), None).unwrap();
3302 db.put("t", "1", serde_json::json!({
3303 "count": 5, "min": 1, "max": 9, "sum": 3, "avg": 2,
3304 "value": "keep", "limit": 7, "offset": 8, "group": "g1", "search": "s",
3305 }), vec![], None, None).unwrap();
3306 db.put("t", "2", serde_json::json!({
3307 "count": 1, "min": 0, "max": 2, "sum": 0, "avg": 0,
3308 "value": "drop", "limit": 0, "offset": 0, "group": "g2", "search": "t",
3309 }), vec![], None, None).unwrap();
3310
3311 for (nql, want) in [
3312 ("FROM t WHERE count > 3", "1"),
3313 ("FROM t WHERE min = 1", "1"),
3314 ("FROM t WHERE max >= 9", "1"),
3315 ("FROM t WHERE sum = 3", "1"),
3316 ("FROM t WHERE avg = 2", "1"),
3317 (r#"FROM t WHERE value = "keep""#, "1"),
3318 ("FROM t WHERE limit = 7", "1"),
3319 ("FROM t WHERE offset = 8", "1"),
3320 (r#"FROM t WHERE group = "g1""#, "1"),
3321 ] {
3322 let (rows, _) = query(&db, nql).unwrap();
3323 assert_eq!(rows.len(), 1, "`{}` matched {} rows", nql, rows.len());
3324 assert_eq!(rows[0]["_id"], want, "`{}`", nql);
3325 }
3326
3327 let (ord, _) = query(&db, "FROM t ORDER BY count DESC").unwrap();
3329 assert_eq!(ord[0]["_id"], "1");
3330 let (grp, _) = query(&db, "FROM t GROUP BY group COUNT").unwrap();
3331 assert_eq!(grp.len(), 2);
3332 let keys: Vec<&str> = grp.iter().filter_map(|r| r["group"].as_str()).collect();
3333 assert!(keys.contains(&"g1") && keys.contains(&"g2"), "{:?}", grp);
3334 }
3335
3336 #[test]
3339 fn keyword_matching_stays_case_insensitive() {
3340 let dir = tempdir().unwrap();
3341 let db = Db::open(dir.path(), None).unwrap();
3342 db.put("t", "1", serde_json::json!({"Count": 5, "n": 1}),
3343 vec![], None, None).unwrap();
3344 let (rows, _) = query(&db, "from t where Count = 5 order by n").unwrap();
3346 assert_eq!(rows.len(), 1);
3347 let (miss, _) = query(&db, "FROM t WHERE count = 5").unwrap();
3349 assert!(miss.is_empty(), "`count` and `Count` are distinct field names");
3350 }
3351
3352 #[test]
3353 fn two_aggregates_is_an_error() {
3354 let (_tmp, db) = setup();
3355 assert!(query(&db, "FROM blocks COUNT SUM n_tx").is_err());
3356 assert!(query(&db, "FROM blocks GROUP BY height COUNT SUM n_tx").is_err());
3357 }
3358
3359 #[test]
3360 fn bare_aggregate_with_having() {
3361 let (_tmp, db) = setup();
3362 let (keep, _) = query(&db, "FROM blocks COUNT HAVING count > 3").unwrap();
3363 assert_eq!(keep.len(), 1);
3364 let (drop, _) = query(&db, "FROM blocks COUNT HAVING count > 99").unwrap();
3365 assert!(drop.is_empty());
3366 }
3367
3368 fn setup_items() -> (tempfile::TempDir, Db) {
3373 let dir = tempdir().unwrap();
3374 let db = Db::open(dir.path(), None).unwrap();
3375 for i in 0..6 {
3376 db.put("items", &i.to_string(),
3377 serde_json::json!({"cat": if i < 3 {"x"} else {"y"}, "price": i * 5}),
3378 vec![], None, None).unwrap();
3379 }
3380 (dir, db)
3381 }
3382
3383 fn group(rows: &[Value], field: &str, key: &str) -> Value {
3384 rows.iter()
3385 .find(|r| r[field] == Value::String(key.to_string()))
3386 .unwrap_or_else(|| panic!("no group {:?} in {:?}", key, rows))
3387 .clone()
3388 }
3389
3390 #[test]
3396 fn group_by_aggregates_the_target_field_not_the_group_field() {
3397 let (_tmp, db) = setup_items();
3398
3399 let (mins, _) = query(&db, "FROM items GROUP BY cat MIN price").unwrap();
3400 assert_eq!(group(&mins, "cat", "x")["min_price"], json!(0));
3401 assert_eq!(group(&mins, "cat", "y")["min_price"], json!(15));
3402
3403 let (maxs, _) = query(&db, "FROM items GROUP BY cat MAX price").unwrap();
3404 assert_eq!(group(&maxs, "cat", "y")["max_price"], json!(25));
3405 assert_eq!(group(&maxs, "cat", "x")["max_price"], json!(10));
3406
3407 let (sums, _) = query(&db, "FROM items GROUP BY cat SUM price").unwrap();
3408 assert_eq!(group(&sums, "cat", "x")["sum_price"], json!(15)); assert_eq!(group(&sums, "cat", "y")["sum_price"], json!(60)); let (avgs, _) = query(&db, "FROM items GROUP BY cat AVG price").unwrap();
3412 assert_eq!(group(&avgs, "cat", "x")["avg_price"], json!(5.0));
3413 assert_eq!(group(&avgs, "cat", "y")["avg_price"], json!(20.0));
3414 }
3415
3416 #[test]
3428 fn aggregates_see_node_metadata_not_only_the_payload() {
3429 let (_tmp, db) = setup_items();
3430 let (all, _) = query(&db, "FROM items").unwrap();
3431 let want = all.iter().filter_map(|r| r.get("_seq")?.as_i64()).max().unwrap();
3432
3433 let (rows, _) = query(&db, "FROM items MAX _seq").unwrap();
3434 assert_eq!(rows[0]["max__seq"], json!(want),
3435 "MAX _seq must equal the highest sequence in the result");
3436 assert_ne!(rows[0]["max__seq"], Value::Null, "a null here is a silent wrong answer");
3437
3438 let (rows, _) = query(&db, "FROM items MIN _seq").unwrap();
3439 assert_eq!(rows[0]["min__seq"], json!(
3440 all.iter().filter_map(|r| r.get("_seq")?.as_i64()).min().unwrap()));
3441
3442 let (rows, _) = query(&db, "FROM items GROUP BY _seq COUNT").unwrap();
3444 assert_eq!(rows.len(), all.len(), "one group per distinct sequence");
3445 assert!(rows.iter().all(|r| r["_seq"] != Value::Null),
3446 "the group key must be the sequence, not null");
3447 }
3448
3449 #[test]
3453 fn group_by_emits_python_parity_keys_and_the_value_alias() {
3454 let (_tmp, db) = setup_items();
3455 let (rows, _) = query(&db, "FROM items GROUP BY cat SUM price").unwrap();
3456 let x = group(&rows, "cat", "x");
3457 assert_eq!(x["sum_price"], json!(15), "python-parity key");
3458 assert_eq!(x["value"], json!(15), "back-compat alias must agree");
3459 assert_eq!(x["count"], json!(3), "count is the group size");
3460 }
3461
3462 #[test]
3466 fn count_is_group_size_while_aggregate_skips_non_numeric() {
3467 let dir = tempdir().unwrap();
3468 let db = Db::open(dir.path(), None).unwrap();
3469 db.put("t", "1", serde_json::json!({"g": "a", "n": 10}), vec![], None, None).unwrap();
3470 db.put("t", "2", serde_json::json!({"g": "a", "n": 20}), vec![], None, None).unwrap();
3471 db.put("t", "3", serde_json::json!({"g": "a", "n": "N/A"}), vec![], None, None).unwrap();
3472
3473 let (rows, _) = query(&db, "FROM t GROUP BY g AVG n").unwrap();
3474 let a = group(&rows, "g", "a");
3475 assert_eq!(a["count"], json!(3), "every row counts toward the group");
3476 assert_eq!(a["avg_n"], json!(15.0), "only the two numeric rows average");
3477 }
3478
3479 #[test]
3481 fn empty_aggregate_input_is_null() {
3482 let dir = tempdir().unwrap();
3483 let db = Db::open(dir.path(), None).unwrap();
3484 db.put("t", "1", serde_json::json!({"g": "a", "n": "x"}), vec![], None, None).unwrap();
3485 let (rows, _) = query(&db, "FROM t GROUP BY g MIN n").unwrap();
3486 assert_eq!(rows[0]["min_n"], Value::Null);
3487 assert_eq!(rows[0]["count"], json!(1));
3488 }
3489
3490 #[test]
3493 fn bare_group_by_without_an_aggregate_counts() {
3494 let (_tmp, db) = setup_items();
3495 let (rows, _) = query(&db, "FROM items GROUP BY cat").unwrap();
3496 assert_eq!(rows.len(), 2);
3497 assert_eq!(group(&rows, "cat", "x")["count"], json!(3));
3498 assert_eq!(group(&rows, "cat", "y")["count"], json!(3));
3499 }
3500
3501 #[test]
3503 fn aggregate_without_a_target_field_is_an_error() {
3504 let (_tmp, db) = setup_items();
3505 for bad in [
3506 "FROM items GROUP BY cat SUM",
3507 "FROM items GROUP BY cat AVG",
3508 "FROM items GROUP BY cat MIN",
3509 ] {
3510 assert!(query(&db, bad).is_err(), "`{}` must be rejected", bad);
3511 }
3512 }
3513
3514 #[test]
3517 fn group_order_is_stable_across_runs() {
3518 let (_tmp, db) = setup_items();
3519 let first = query(&db, "FROM items GROUP BY cat SUM price").unwrap().0;
3520 for _ in 0..8 {
3521 let again = query(&db, "FROM items GROUP BY cat SUM price").unwrap().0;
3522 assert_eq!(first, again);
3523 }
3524 }
3525
3526 #[test]
3528 fn group_by_after_an_in_predicate() {
3529 let (_tmp, db) = setup_items();
3530 let (rows, _) = query(
3531 &db, "FROM items WHERE price IN (0, 5, 25) GROUP BY cat SUM price").unwrap();
3532 assert_eq!(group(&rows, "cat", "x")["sum_price"], json!(5));
3533 assert_eq!(group(&rows, "cat", "y")["sum_price"], json!(25));
3534 }
3535
3536 #[test]
3537 fn search() {
3538 let (_tmp, db) = setup();
3539 let (rows, _) = query(&db, r#"FROM blocks SEARCH "0003""#).unwrap();
3540 assert_eq!(rows.len(), 1);
3541 }
3542
3543 #[test]
3544 fn as_of() {
3545 let dir = tempdir().unwrap();
3546 let db = Db::open(dir.path(), None).unwrap();
3547 let v1 = db.put("docs", "x", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
3548 db.put("docs", "x", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
3549 let (rows, _) = query(&db, &format!("FROM docs AS OF {}", v1.seq)).unwrap();
3550 assert_eq!(rows[0]["v"], 1);
3551 }
3552
3553 #[test]
3554 fn valid_as_of() {
3555 let dir = tempdir().unwrap();
3556 let db = Db::open(dir.path(), None).unwrap();
3557 db.put("events", "e1", serde_json::json!({"type": "a"}), vec![],
3558 Some("2025-01-01".to_string()), Some("2025-06-01".to_string())).unwrap();
3559 db.put("events", "e2", serde_json::json!({"type": "b"}), vec![],
3560 Some("2026-01-01".to_string()), None).unwrap();
3561 let (rows, _) = query(&db, r#"FROM events VALID AS OF "2025-03-01""#).unwrap();
3562 assert_eq!(rows.len(), 1);
3563 assert_eq!(rows[0]["type"], "a");
3564 }
3565
3566 #[test]
3569 fn escaped_quote_matches_a_value_containing_a_quote() {
3570 let dir = tempdir().unwrap();
3571 let db = Db::open(dir.path(), None).unwrap();
3572 db.put("m", "q", serde_json::json!({ "name": "say \"hi\"" }), vec![], None, None)
3573 .unwrap();
3574 db.put("m", "p", serde_json::json!({ "name": "plain" }), vec![], None, None).unwrap();
3575
3576 let (rows, count) = query(&db, r#"FROM m WHERE name = "say \"hi\"""#).unwrap();
3578 assert_eq!(count, 1, "the escaped-quote literal matches exactly one row");
3579 assert_eq!(rows[0]["_id"], "q");
3580 }
3581
3582 #[test]
3583 fn raw_backslash_still_matches_literally() {
3584 let dir = tempdir().unwrap();
3588 let db = Db::open(dir.path(), None).unwrap();
3589 db.put("m", "b", serde_json::json!({ "p": "back\\slash" }), vec![], None, None).unwrap();
3590
3591 let (rows, count) = query(&db, r#"FROM m WHERE p = "back\slash""#).unwrap();
3592 assert_eq!(count, 1, "a raw backslash literal matches as before");
3593 assert_eq!(rows[0]["_id"], "b");
3594 }
3595
3596 #[test]
3597 fn a_quote_can_no_longer_inject_trailing_clauses() {
3598 let dir = tempdir().unwrap();
3603 let db = Db::open(dir.path(), None).unwrap();
3604 db.put("m", "x", serde_json::json!({ "v": "a\"b" }), vec![], None, None).unwrap();
3605 let (rows, count) = query(&db, r#"FROM m WHERE v = "a\"b""#).unwrap();
3606 assert_eq!(count, 1);
3607 assert_eq!(rows[0]["_id"], "x");
3608 }
3609}
3610
3611#[cfg(test)]
3612mod tests_traverse {
3613 use super::*;
3614 use tempfile::tempdir;
3615 use crate::db::Db;
3616
3617 #[test]
3618 fn traverse_one_hop() {
3619 let db = Db::in_memory();
3620 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
3621 db.put("driver", "d2", serde_json::json!({"name": "Carol"}), vec![], None, None).unwrap();
3622 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
3623 db.put("trip", "t2", serde_json::json!({"status": "ok"}), vec![], None, None).unwrap();
3624
3625 db.link("driver:d1", "handles", "trip:t1").unwrap();
3626 db.link("driver:d1", "handles", "trip:t2").unwrap();
3627
3628 let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3629 assert_eq!(count, 2);
3630 let ids: std::collections::HashSet<&str> = rows.iter()
3631 .filter_map(|r| r["_id"].as_str())
3632 .collect();
3633 assert!(ids.contains("t1") && ids.contains("t2"));
3634 }
3635
3636 #[test]
3637 fn traverse_returns_empty_when_no_links() {
3638 let db = Db::in_memory();
3639 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
3640 let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3641 assert_eq!(count, 0);
3642 assert!(rows.is_empty());
3643 }
3644
3645 #[test]
3646 fn traverse_multi_source() {
3647 let db = Db::in_memory();
3649 db.put("driver", "d1", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
3650 db.put("driver", "d2", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
3651 db.put("trip", "t1", serde_json::json!({"n": 1}), vec![], None, None).unwrap();
3652 db.put("trip", "t2", serde_json::json!({"n": 2}), vec![], None, None).unwrap();
3653 db.put("trip", "t3", serde_json::json!({"n": 3}), vec![], None, None).unwrap();
3654
3655 db.link("driver:d1", "handles", "trip:t1").unwrap();
3656 db.link("driver:d1", "handles", "trip:t2").unwrap();
3657 db.link("driver:d2", "handles", "trip:t3").unwrap();
3658
3659 let (_rows, count) = query(&db, r#"FROM driver WHERE status = "active" TRAVERSE handles"#).unwrap();
3660 assert_eq!(count, 3);
3661 }
3662
3663 #[test]
3664 fn traverse_nql_keyword_case_insensitive() {
3665 let db = Db::in_memory();
3667 db.put("driver", "d1", serde_json::json!({}), vec![], None, None).unwrap();
3668 db.put("trip", "t1", serde_json::json!({}), vec![], None, None).unwrap();
3669 db.link("driver:d1", "handles", "trip:t1").unwrap();
3670 let (r1, c1) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3672 assert_eq!(c1, 1);
3673 let (r2, c2) = query(&db, r#"FROM driver WHERE _id = "d1" traverse handles"#).unwrap();
3675 assert_eq!(c2, 1);
3676 assert_eq!(r1[0]["_id"], r2[0]["_id"]);
3677 }
3678
3679 #[test]
3680 fn traverse_durable() {
3681 let dir = tempdir().unwrap();
3682 {
3683 let db = Db::open(dir.path(), None).unwrap();
3684 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
3685 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
3686 db.link("driver:d1", "handles", "trip:t1").unwrap();
3687 }
3688 let db2 = Db::open(dir.path(), None).unwrap();
3689 db2.startup_ready.store(true, std::sync::atomic::Ordering::SeqCst);
3690 let (rows, count) = query(&db2, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3691 assert_eq!(count, 1);
3692 assert_eq!(rows[0]["_id"], "t1");
3693 }
3694}