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(bad) = unsupported_regex_char(&pattern) {
501 bail!("WHERE: regex {:?} uses {:?}, which this engine does not \
502 implement. The supported subset is ^ $ . and literal text \
503 — enough for catalogue filters like '^pg_'. Matching the \
504 rest approximately would silently include or exclude rows, \
505 so it is refused instead", pattern, bad);
506 }
507 return Ok(Pred::Regex {
508 field,
509 pattern,
510 negated: op.starts_with('!'),
511 ci: op.ends_with('*'),
512 });
513 }
514
515 let value = self.parse_value()?;
516 Ok(Pred::Cmp { field, op, value })
517 }
518
519 fn parse(&mut self) -> Result<Query> {
520 self.expect_kw("FROM")?;
521 let coll = match self.advance() {
522 Tok::Ident(s) | Tok::Kw(_, s) => s,
523 other => bail!("expected collection name, got {:?}", other),
524 };
525
526 let mut q = Query {
527 coll, as_of: None, valid_as_of: None,
528 where_: None, search: None,
529 order_by: vec![],
530 limit: None, offset: None,
531 aggregate: None, having: None,
532 trace: None, trace_rev: false,
533 traverse: None,
534 };
535
536 loop {
537 match self.peek() {
538 Tok::Eof => break,
539
540 Tok::Kw(k, _) if k == "AS" => {
541 self.advance();
542 self.expect_kw("OF")?;
543 match self.advance() {
544 Tok::Num(n) => q.as_of = Some(n as u64),
545 other => bail!("AS OF expects sequence number, got {:?}", other),
546 }
547 }
548
549 Tok::Kw(k, _) if k == "VALID" => {
550 self.advance();
551 self.expect_kw("AS")?;
552 self.expect_kw("OF")?;
553 match self.advance() {
554 Tok::Str(s) => q.valid_as_of = Some(s),
555 other => bail!("VALID AS OF expects date string, got {:?}", other),
556 }
557 }
558
559 Tok::Kw(k, _) if k == "WHERE" => {
560 self.advance();
561 let pred = self.parse_pred()?;
562 q.where_ = Some(match q.where_.take() {
565 None => pred,
566 Some(prev) => Pred::And(vec![prev, pred]),
567 });
568 }
569
570 Tok::Kw(k, _) if k == "SEARCH" => {
571 self.advance();
572 match self.advance() {
573 Tok::Str(s) => q.search = Some(s),
574 other => bail!("SEARCH expects quoted string, got {:?}", other),
575 }
576 }
577
578 Tok::Kw(k, _) if k == "ORDER" => {
579 self.advance();
580 self.expect_kw("BY")?;
581 loop {
584 let field = self.parse_field("ORDER BY")?;
585 let desc = if self.eat_kw("DESC") {
589 true
590 } else {
591 self.eat_kw("ASC");
592 false
593 };
594 q.order_by.push(OrderKey { field, desc });
595 if matches!(self.peek(), Tok::Punct(',')) { self.advance(); continue; }
596 break;
597 }
598 }
599
600 Tok::Kw(k, _) if k == "LIMIT" => {
601 self.advance();
602 match self.advance() {
603 Tok::Num(n) if n >= 0.0 => q.limit = Some(n as usize),
604 other => bail!("LIMIT expects a non-negative number, got {:?}", other),
605 }
606 }
607
608 Tok::Kw(k, _) if k == "OFFSET" => {
609 self.advance();
610 match self.advance() {
611 Tok::Num(n) if n >= 0.0 => q.offset = Some(n as usize),
612 other => bail!("OFFSET expects a non-negative number, got {:?}", other),
613 }
614 }
615
616 Tok::Kw(k, _) if k == "HAVING" => {
617 self.advance();
618 let pred = self.parse_pred()?;
619 q.having = Some(match q.having.take() {
620 None => pred,
621 Some(prev) => Pred::And(vec![prev, pred]),
622 });
623 }
624
625 Tok::Kw(k, _) if matches!(k.as_str(), "COUNT" | "SUM" | "AVG" | "MIN" | "MAX") => {
628 let agg = self.parse_agg_kw()?;
629 let agg_field = match agg {
630 GroupAgg::Count => None,
631 _ => Some(self.parse_field("aggregate")?),
632 };
633 if q.aggregate.is_some() {
634 bail!("only one aggregate per query");
635 }
636 q.aggregate = Some(Aggregate { group_field: None, agg, agg_field });
637 }
638
639 Tok::Kw(k, _) if k == "GROUP" => {
640 self.advance();
641 self.expect_kw("BY")?;
642 let field = match self.advance() {
643 Tok::Ident(s) | Tok::Kw(_, s) => s,
644 other => bail!("GROUP BY: expected field, got {:?}", other),
645 };
646 let agg = if matches!(self.peek(),
651 Tok::Kw(a, _) if matches!(a.as_str(), "COUNT"|"SUM"|"AVG"|"MIN"|"MAX"))
652 {
653 self.parse_agg_kw()?
654 } else {
655 GroupAgg::Count
656 };
657 let agg_field = match agg {
664 GroupAgg::Count => None,
665 _ => Some(self.parse_field("GROUP BY aggregate")?),
666 };
667 if q.aggregate.is_some() {
668 bail!("only one aggregate per query");
669 }
670 q.aggregate = Some(Aggregate {
671 group_field: Some(field), agg, agg_field,
672 });
673 }
674
675 Tok::Kw(k, _) if k == "TRACE" => {
676 self.advance();
677 let edge = match self.advance() {
678 Tok::Ident(s) | Tok::Kw(_, s) => s,
679 other => bail!("TRACE: expected edge type, got {:?}", other),
680 };
681 q.trace = Some(edge);
682 if let Tok::Kw(k, _) = self.peek() {
683 if k == "REVERSE" { self.advance(); q.trace_rev = true; }
684 }
685 }
686
687 Tok::Kw(k, _) if k == "TRAVERSE" => {
688 self.advance();
689 let rel = match self.advance() {
690 Tok::Ident(s) | Tok::Kw(_, s) => s,
691 other => bail!("TRAVERSE: expected relation name, got {:?}", other),
692 };
693 q.traverse = Some(rel);
694 }
695
696 other => bail!(
699 "unexpected {:?} in query. Expected one of: AS OF, VALID AS OF, \
700 WHERE, SEARCH, ORDER BY, LIMIT, OFFSET, GROUP BY, HAVING, \
701 COUNT, SUM, AVG, MIN, MAX, TRACE, TRAVERSE",
702 other
703 ),
704 }
705 }
706
707 Ok(q)
708 }
709}
710
711fn field_value(node: &Node, field: &str) -> Value {
716 match field {
717 "_id" => Value::String(node.id.clone()),
718 "_coll" => Value::String(node.coll.clone()),
719 "_hash" => Value::String(node.hash.clone()),
720 "_seq" => json!(node.seq),
721 _ => node.data.get(field).cloned().unwrap_or(Value::Null),
722 }
723}
724
725fn cmp_op(a: &Value, op: &str, b: &Value) -> bool {
726 if matches!(op, "<" | "<=" | ">" | ">=") && a.is_null() {
747 return false;
748 }
749 let a = OrderedValue::from(a);
750 let b = OrderedValue::from(b);
751 match op {
752 "=" => a == b,
753 "!=" => a != b,
754 ">" => a > b,
755 "<" => a < b,
756 ">=" => a >= b,
757 "<=" => a <= b,
758 _ => false,
759 }
760}
761
762fn as_text(v: &Value) -> String {
766 match v {
767 Value::String(s) => s.clone(),
768 Value::Null => String::new(),
769 other => other.to_string(),
770 }
771}
772
773fn like_match(value: &str, pattern: &str, ci: bool) -> bool {
778 let (v, p): (Vec<char>, Vec<char>) = if ci {
779 (value.to_lowercase().chars().collect(), pattern.to_lowercase().chars().collect())
780 } else {
781 (value.chars().collect(), pattern.chars().collect())
782 };
783
784 let mut vi = 0usize;
785 let mut pi = 0usize;
786 let mut star: Option<(usize, usize)> = None;
788
789 while vi < v.len() {
790 if pi < p.len() && (p[pi] == '_' || p[pi] == v[vi]) {
791 vi += 1;
792 pi += 1;
793 } else if pi < p.len() && p[pi] == '%' {
794 star = Some((pi, vi));
795 pi += 1;
796 } else if let Some((sp, sv)) = star {
797 pi = sp + 1;
799 vi = sv + 1;
800 star = Some((sp, vi));
801 } else {
802 return false;
803 }
804 }
805 while pi < p.len() && p[pi] == '%' { pi += 1; }
807 pi == p.len()
808}
809
810fn unsupported_regex_char(pattern: &str) -> Option<char> {
816 const UNSUPPORTED: &[char] =
820 &['*', '+', '?', '[', ']', '(', ')', '{', '}', '|', '\\'];
821 pattern.chars().find(|c| UNSUPPORTED.contains(c))
822}
823
824fn regex_match(value: &str, pattern: &str, ci: bool) -> bool {
830 let (v, p): (Vec<char>, Vec<char>) = if ci {
831 (value.to_lowercase().chars().collect(), pattern.to_lowercase().chars().collect())
832 } else {
833 (value.chars().collect(), pattern.chars().collect())
834 };
835
836 let anchored_start = p.first() == Some(&'^');
837 let anchored_end = p.last() == Some(&'$');
844 let body = &p[usize::from(anchored_start)..p.len() - usize::from(anchored_end)];
845
846 let matches_at = |start: usize| -> bool {
848 if start + body.len() > v.len() {
849 return false;
850 }
851 body.iter().enumerate().all(|(i, pc)| *pc == '.' || *pc == v[start + i])
852 };
853
854 match (anchored_start, anchored_end) {
855 (true, true) => v.len() == body.len() && matches_at(0),
856 (true, false) => matches_at(0),
857 (false, true) => v.len() >= body.len() && matches_at(v.len() - body.len()),
858 (false, false) => (0..=v.len().saturating_sub(body.len())).any(matches_at),
861 }
862}
863
864pub fn regex_match_pub(value: &str, pattern: &str, ci: bool) -> bool {
872 regex_match(value, pattern, ci)
873}
874
875pub fn unsupported_regex_char_pub(pattern: &str) -> Option<char> {
877 unsupported_regex_char(pattern)
878}
879
880pub fn like_match_pub(value: &str, pattern: &str, ci: bool) -> bool {
882 like_match(value, pattern, ci)
883}
884
885fn eval_pred_with(get: &dyn Fn(&str) -> Value, pred: &Pred) -> bool {
892 match pred {
893 Pred::Cmp { field, op, value } => cmp_op(&get(field), op, value),
894
895 Pred::In { field, values, negated } => {
896 let fv = get(field);
897 let hit = values.iter().any(|v| cmp_op(&fv, "=", v));
898 hit != *negated
899 }
900
901 Pred::Between { field, low, high, negated } => {
902 let fv = get(field);
903 let hit = cmp_op(&fv, ">=", low) && cmp_op(&fv, "<=", high);
905 hit != *negated
906 }
907
908 Pred::Like { field, pattern, negated, ci } => {
909 let fv = get(field);
910 if fv.is_null() { return false; }
914 let hit = like_match(&as_text(&fv), pattern, *ci);
915 hit != *negated
916 }
917
918 Pred::Regex { field, pattern, negated, ci } => {
919 let fv = get(field);
920 if fv.is_null() { return false; }
925 let hit = regex_match(&as_text(&fv), pattern, *ci);
926 hit != *negated
927 }
928
929 Pred::IsNull { field, negated } => {
930 get(field).is_null() != *negated
934 }
935
936 Pred::And(terms) => terms.iter().all(|t| eval_pred_with(get, t)),
937 Pred::Or(terms) => terms.iter().any(|t| eval_pred_with(get, t)),
938 Pred::Not(inner) => !eval_pred_with(get, inner),
939 }
940}
941
942fn eval_pred(node: &Node, pred: &Pred) -> bool {
943 eval_pred_with(&|f| field_value(node, f), pred)
944}
945
946fn eval_pred_json(obj: &Value, pred: &Pred) -> bool {
948 eval_pred_with(&|f| obj.get(f).cloned().unwrap_or(Value::Null), pred)
949}
950
951fn sort_by_keys<T>(rows: &mut [T], keys: &[OrderKey], get: impl Fn(&T, &str) -> Value) {
954 rows.sort_by(|a, b| {
955 for k in keys {
956 let av = OrderedValue::from(&get(a, &k.field));
957 let bv = OrderedValue::from(&get(b, &k.field));
958 let ord = if k.desc { bv.cmp(&av) } else { av.cmp(&bv) };
959 if ord != std::cmp::Ordering::Equal {
960 return ord;
961 }
962 }
963 std::cmp::Ordering::Equal
964 });
965}
966
967fn paginate<T>(rows: Vec<T>, offset: Option<usize>, limit: Option<usize>) -> Vec<T> {
972 let mut it = rows;
973 if let Some(off) = offset {
974 if off >= it.len() {
975 return vec![];
976 }
977 it.drain(..off);
978 }
979 if let Some(n) = limit {
980 it.truncate(n);
981 }
982 it
983}
984
985fn aggregate_rows(rows: &[Node], spec: &Aggregate) -> Vec<Value> {
998 struct Group { count: usize, nums: Vec<f64>, ints: Vec<i64>, all_int: bool }
1008
1009 let mut order: Vec<String> = vec![];
1012 let mut groups: HashMap<String, Group> = HashMap::new();
1013
1014 const WHOLE: &str = "";
1017
1018 for node in rows {
1019 let key = match spec.group_field {
1027 None => WHOLE.to_string(),
1028 Some(ref gf) => match field_value(node, gf) {
1029 Value::Null => "null".to_string(),
1030 v => as_text(&v),
1031 },
1032 };
1033 let entry = groups.entry(key.clone()).or_insert_with(|| {
1034 order.push(key.clone());
1035 Group { count: 0, nums: vec![], ints: vec![], all_int: true }
1036 });
1037 entry.count += 1;
1038 if let Some(ref af) = spec.agg_field {
1039 if let Value::Number(n) = field_value(node, af) {
1042 if let Some(i) = n.as_i64() {
1043 entry.ints.push(i);
1044 entry.nums.push(i as f64);
1045 } else if let Some(f) = n.as_f64() {
1046 entry.all_int = false;
1047 entry.nums.push(f);
1048 }
1049 }
1050 }
1051 }
1052
1053 if spec.group_field.is_none() && order.is_empty() {
1057 order.push(WHOLE.to_string());
1058 groups.insert(WHOLE.to_string(),
1059 Group { count: 0, nums: vec![], ints: vec![], all_int: true });
1060 }
1061
1062 order.into_iter().map(|k| {
1063 let g = &groups[&k];
1064 let mut obj = serde_json::Map::new();
1065 if let Some(ref gf) = spec.group_field {
1066 obj.insert(gf.clone(), Value::String(k.clone()));
1067 }
1068 obj.insert("count".to_string(), json!(g.count));
1069
1070 let int_path = g.all_int && !g.ints.is_empty();
1074 let agg_val: Value = match spec.agg {
1075 GroupAgg::Count => json!(g.count),
1076 _ if g.nums.is_empty() => Value::Null,
1077 GroupAgg::Sum if int_path => {
1080 match g.ints.iter().try_fold(0i64, |a, &b| a.checked_add(b)) {
1081 Some(t) => json!(t),
1082 None => json!(g.nums.iter().sum::<f64>()),
1083 }
1084 }
1085 GroupAgg::Min if int_path => json!(g.ints.iter().min().copied().unwrap()),
1086 GroupAgg::Max if int_path => json!(g.ints.iter().max().copied().unwrap()),
1087 GroupAgg::Sum => json!(g.nums.iter().sum::<f64>()),
1088 GroupAgg::Avg => json!(g.nums.iter().sum::<f64>() / g.nums.len() as f64),
1090 GroupAgg::Min => json!(g.nums.iter().cloned().fold(f64::INFINITY, f64::min)),
1091 GroupAgg::Max => json!(g.nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max)),
1092 };
1093
1094 if let Some(ref af) = spec.agg_field {
1096 obj.insert(format!("{}_{}", spec.agg.name(), af), agg_val.clone());
1097 }
1098 obj.insert("value".to_string(), agg_val);
1103 Value::Object(obj)
1104 }).collect()
1105}
1106
1107#[derive(Debug, Clone)]
1116enum IndexPlan {
1117 Range {
1119 field: String,
1120 low: Option<Value>,
1121 high: Option<Value>,
1122 low_incl: bool,
1123 high_incl: bool,
1124 },
1125 Values { field: String, values: Vec<Value> },
1127}
1128
1129impl IndexPlan {
1130 fn field(&self) -> &str {
1131 match self {
1132 IndexPlan::Range { field, .. } => field,
1133 IndexPlan::Values { field, .. } => field,
1134 }
1135 }
1136}
1137
1138fn collect_index_constraints(pred: &Pred, out: &mut Vec<IndexPlan>) {
1151 match pred {
1152 Pred::And(terms) => {
1153 for t in terms {
1154 collect_index_constraints(t, out);
1155 }
1156 }
1157
1158 Pred::Cmp { field, op, value } => {
1159 if field == "_id" {
1161 return;
1162 }
1163 match op.as_str() {
1164 "=" => out.push(IndexPlan::Values {
1165 field: field.clone(),
1166 values: vec![value.clone()],
1167 }),
1168 ">" | ">=" => out.push(IndexPlan::Range {
1169 field: field.clone(),
1170 low: Some(value.clone()),
1171 high: None,
1172 low_incl: op == ">=",
1173 high_incl: true,
1174 }),
1175 "<" | "<=" => out.push(IndexPlan::Range {
1176 field: field.clone(),
1177 low: None,
1178 high: Some(value.clone()),
1179 low_incl: true,
1180 high_incl: op == "<=",
1181 }),
1182 _ => {}
1185 }
1186 }
1187
1188 Pred::Between { field, low, high, negated: false } => {
1189 out.push(IndexPlan::Range {
1190 field: field.clone(),
1191 low: Some(low.clone()),
1192 high: Some(high.clone()),
1193 low_incl: true, high_incl: true,
1195 });
1196 }
1197
1198 Pred::In { field, values, negated: false } => {
1199 out.push(IndexPlan::Values {
1200 field: field.clone(),
1201 values: values.clone(),
1202 });
1203 }
1204
1205 _ => {}
1211 }
1212}
1213
1214fn choose_index_plan(db: &Db, coll: &str, pred: &Pred) -> Option<IndexPlan> {
1221 let mut raw = vec![];
1222 collect_index_constraints(pred, &mut raw);
1223 raw.retain(|p| db.has_sorted_index(coll, p.field()));
1224 if raw.is_empty() {
1225 return None;
1226 }
1227
1228 let mut merged: Vec<IndexPlan> = vec![];
1230 for plan in raw {
1231 let field = plan.field().to_string();
1232 let existing = merged.iter().position(|m| m.field() == field);
1233 match (existing, plan) {
1234 (None, p) => merged.push(p),
1235
1236 (Some(i), IndexPlan::Range { low, high, low_incl, high_incl, .. }) => {
1238 if let IndexPlan::Range {
1239 low: ref mut elow, high: ref mut ehigh,
1240 low_incl: ref mut eli, high_incl: ref mut ehi, ..
1241 } = merged[i] {
1242 if let Some(l) = low {
1243 let tighter = match elow {
1244 None => true,
1245 Some(cur) => OrderedValue::from(&l) > OrderedValue::from(&*cur),
1246 };
1247 if tighter { *elow = Some(l); *eli = low_incl; }
1248 }
1249 if let Some(h) = high {
1250 let tighter = match ehigh {
1251 None => true,
1252 Some(cur) => OrderedValue::from(&h) < OrderedValue::from(&*cur),
1253 };
1254 if tighter { *ehigh = Some(h); *ehi = high_incl; }
1255 }
1256 }
1257 }
1261
1262 (Some(i), p @ IndexPlan::Values { .. }) => {
1264 if matches!(merged[i], IndexPlan::Range { .. }) {
1265 merged[i] = p;
1266 }
1267 }
1268 }
1269 }
1270
1271 let mut best: Option<(usize, IndexPlan)> = None;
1275 for plan in merged {
1276 let card = match &plan {
1277 IndexPlan::Range { field, low, high, low_incl, high_incl } => db
1278 .range_cardinality(coll, field, low.as_ref(), high.as_ref(),
1279 *low_incl, *high_incl)
1280 .unwrap_or(usize::MAX),
1281 IndexPlan::Values { field, values } => values
1282 .iter()
1283 .map(|v| db.range_cardinality(coll, field, Some(v), Some(v), true, true)
1284 .unwrap_or(usize::MAX))
1285 .fold(0usize, |a, b| a.saturating_add(b)),
1286 };
1287 if best.as_ref().map(|(c, _)| card < *c).unwrap_or(true) {
1288 best = Some((card, plan));
1289 }
1290 }
1291 best.map(|(_, p)| p)
1292}
1293
1294fn id_point_lookup(pred: &Pred) -> Option<String> {
1295 match pred {
1296 Pred::Cmp { field, op, value } if field == "_id" && op == "=" => {
1297 if let Value::String(s) = value { Some(s.clone()) } else { None }
1298 }
1299 Pred::And(terms) => terms.iter().find_map(id_point_lookup),
1300 _ => None,
1301 }
1302}
1303
1304fn matches_valid_as_of(node: &Node, date: &str) -> bool {
1305 let from_ok = node.valid_from.as_deref().map(|f| f <= date).unwrap_or(true);
1309 let to_ok = node.valid_to.as_deref().map(|t| t > date).unwrap_or(true);
1310 from_ok && to_ok
1311}
1312
1313fn node_contains_text(node: &Node, text: &str) -> bool {
1314 let s = node.data.to_string().to_lowercase();
1315 s.contains(&text.to_lowercase())
1316}
1317
1318pub fn node_to_json(node: &Node) -> Value {
1322 let mut obj = if let Value::Object(m) = &node.data {
1323 m.clone()
1324 } else {
1325 serde_json::Map::new()
1326 };
1327 obj.insert("_id".to_string(), Value::String(node.id.clone()));
1328 obj.insert("_hash".to_string(), Value::String(node.hash.clone()));
1329 obj.insert("_seq".to_string(), json!(node.seq));
1330 obj.insert("_coll".to_string(), Value::String(node.coll.clone()));
1331 if let Some(ref vf) = node.valid_from {
1332 obj.insert("_valid_from".to_string(), Value::String(vf.clone()));
1333 }
1334 if let Some(ref vt) = node.valid_to {
1335 obj.insert("_valid_to".to_string(), Value::String(vt.clone()));
1336 }
1337 if !node.caused_by.is_empty() {
1338 obj.insert("_caused_by".to_string(), Value::Array(
1339 node.caused_by.iter().map(|h| Value::String(h.clone())).collect()
1340 ));
1341 }
1342 Value::Object(obj)
1343}
1344
1345pub fn parse(nql: &str) -> Result<Query> {
1354 let mut lexer = Lexer::new(nql);
1355 let toks = lexer.tokenize();
1356 let mut parser = Parser::new(toks);
1357 parser.parse()
1358}
1359
1360pub fn execute(db: &Db, nql: &str) -> Result<Vec<Value>> {
1361 let q = parse(nql)?;
1364
1365 let id_eq_fast_path: Option<String> = if q.as_of.is_none() && q.trace.is_none() {
1372 q.where_.as_ref().and_then(id_point_lookup)
1373 } else { None };
1374
1375 let candidates: Vec<Node> = if let Some(ref target_id) = id_eq_fast_path {
1376 db.get(&q.coll, target_id).into_iter().collect()
1378 } else if let Some(seq_target) = q.as_of {
1379 db.list_ids_including_deleted(&q.coll).into_iter()
1389 .filter_map(|id| db.get_as_of(&q.coll, &id, seq_target))
1390 .collect()
1391 } else if let Some(plan) = q.where_.as_ref()
1392 .filter(|_| q.as_of.is_none())
1401 .and_then(|p| choose_index_plan(db, &q.coll, p))
1402 {
1403 let got = match &plan {
1406 IndexPlan::Range { field, low, high, low_incl, high_incl } => db.range_scan(
1407 &q.coll, field, low.as_ref(), high.as_ref(), *low_incl, *high_incl),
1408 IndexPlan::Values { field, values } => db.index_lookup(&q.coll, field, values),
1409 };
1410 match got {
1411 Some(nodes) => nodes,
1412 None => db.list(&q.coll),
1415 }
1416 } else if q.order_by.len() == 1 && q.aggregate.is_none() {
1417 let key = &q.order_by[0];
1432 let has_post_filters = q.where_.is_some()
1433 || q.search.is_some()
1434 || q.valid_as_of.is_some();
1435 let limit = if has_post_filters {
1436 9_999_999
1437 } else {
1438 match q.limit {
1442 Some(n) => n.saturating_add(q.offset.unwrap_or(0)),
1443 None => 9_999_999,
1444 }
1445 };
1446 if key.desc {
1447 db.order_by_desc(&q.coll, &key.field, limit)
1448 } else {
1449 db.order_by_asc(&q.coll, &key.field, limit)
1450 }
1451 } else if let (Some(n), true) = (q.limit, q.where_.is_none()
1452 && q.search.is_none() && q.trace.is_none()
1453 && q.traverse.is_none() && q.aggregate.is_none()
1454 && q.order_by.is_empty() && q.offset.is_none()
1455 && q.valid_as_of.is_none()) {
1456 db.id_index
1461 .list_ids(&q.coll)
1462 .into_iter()
1463 .take(n)
1464 .filter_map(|id| db.get(&q.coll, &id))
1465 .collect()
1466 } else {
1467 db.list(&q.coll)
1469 };
1470
1471 let mut rows: Vec<Node> = candidates.into_iter()
1474 .filter(|n| q.where_.as_ref().map(|p| eval_pred(n, p)).unwrap_or(true))
1475 .filter(|n| q.valid_as_of.as_deref()
1476 .map(|d| matches_valid_as_of(n, d))
1477 .unwrap_or(true))
1478 .filter(|n| q.search.as_deref()
1479 .map(|t| node_contains_text(n, t))
1480 .unwrap_or(true))
1481 .collect();
1482
1483 if let Some(ref _edge_type) = q.trace {
1486 let limit = q.limit.unwrap_or(1000);
1487 let mut traced: Vec<Node> = vec![];
1488 for root in &rows {
1489 let chain = db.trace(&root.hash, q.trace_rev, limit);
1490 traced.extend(chain);
1491 }
1492 rows = traced;
1493 }
1494
1495 if let Some(ref rel) = q.traverse {
1498 let mut traversed: Vec<Node> = vec![];
1499 for root in &rows {
1500 let frm = format!("{}:{}", root.coll, root.id);
1501 let neighbors = db.neighbors(&frm, rel);
1502 traversed.extend(neighbors);
1503 }
1504 rows = traversed;
1505 }
1506
1507 if let Some(ref spec) = q.aggregate {
1525 let mut out = aggregate_rows(&rows, spec);
1526
1527 if let Some(ref pred) = q.having {
1530 out.retain(|row| eval_pred_json(row, pred));
1531 }
1532
1533 if !q.order_by.is_empty() {
1534 sort_by_keys(&mut out, &q.order_by,
1535 |row, f| row.get(f).cloned().unwrap_or(Value::Null));
1536 } else if let Some(ref gf) = spec.group_field {
1537 let gf = gf.clone();
1545 out.sort_by(|a, b| {
1546 as_text(&a.get(&gf).cloned().unwrap_or(Value::Null))
1547 .cmp(&as_text(&b.get(&gf).cloned().unwrap_or(Value::Null)))
1548 });
1549 }
1550
1551 return Ok(paginate(out, q.offset, q.limit));
1552 }
1553
1554 if q.having.is_some() {
1555 bail!("HAVING requires an aggregate — add GROUP BY <field>, or use WHERE \
1556 to filter individual rows");
1557 }
1558
1559 if !q.order_by.is_empty() {
1562 let index_path_held = q.order_by.len() == 1
1566 && q.as_of.is_none()
1567 && q.where_.is_none()
1568 && q.search.is_none()
1569 && q.valid_as_of.is_none()
1570 && q.trace.is_none()
1571 && q.traverse.is_none();
1572 if !index_path_held {
1573 sort_by_keys(&mut rows, &q.order_by,
1574 |n, f| field_value(n, f));
1575 }
1576 }
1577
1578 let rows = paginate(rows, q.offset, q.limit);
1581
1582 Ok(rows.into_iter().map(|n| node_to_json(&n)).collect())
1585}
1586
1587pub fn query(db: &Db, nql: &str) -> Result<(Vec<Value>, usize)> {
1589 let rows = execute(db, nql)?;
1590 let count = rows.len();
1591 Ok((rows, count))
1592}
1593
1594pub fn query_rows(rows: Vec<Value>, nql: &str) -> Result<Vec<Value>> {
1615 let q = parse(nql)?;
1616
1617 for (unsupported, clause) in [
1618 (q.as_of.is_some(), "AS OF"),
1619 (q.valid_as_of.is_some(), "VALID AS OF"),
1620 (q.trace.is_some(), "TRACE"),
1621 (q.traverse.is_some(), "TRAVERSE"),
1622 (q.search.is_some(), "SEARCH"),
1623 (q.aggregate.is_some(), "an aggregate"),
1624 (q.having.is_some(), "HAVING"),
1625 ] {
1626 if unsupported {
1627 bail!("{} is not supported on the catalogue table {:?} — a catalogue \
1628 is synthesised from the current database, so it has no history, \
1629 no causal edges and nothing to aggregate. Query the collection \
1630 itself for those", clause, q.coll);
1631 }
1632 }
1633
1634 let get = |row: &Value, field: &str| -> Value {
1635 row.get(field).cloned().unwrap_or(Value::Null)
1636 };
1637
1638 let mut kept: Vec<Value> = match &q.where_ {
1639 None => rows,
1640 Some(pred) => rows
1641 .into_iter()
1642 .filter(|r| eval_pred_with(&|f| get(r, f), pred))
1643 .collect(),
1644 };
1645
1646 if !q.order_by.is_empty() {
1647 sort_by_keys(&mut kept, &q.order_by, get);
1648 }
1649 Ok(paginate(kept, q.offset, q.limit))
1650}
1651
1652#[cfg(test)]
1653mod tests {
1654 use super::*;
1655 use tempfile::tempdir;
1656 use crate::db::Db;
1657
1658 fn setup() -> (tempfile::TempDir, Db) {
1663 let dir = tempdir().unwrap();
1664 let db = Db::open(dir.path(), None).unwrap();
1665 db.create_sorted_index("blocks", "height");
1666 for h in 1u64..=5 {
1667 db.put("blocks", &h.to_string(),
1668 serde_json::json!({"height": h, "hash": format!("000{}", h), "n_tx": h * 2}),
1669 vec![], None, None).unwrap();
1670 }
1671 (dir, db)
1672 }
1673
1674 #[test]
1675 fn from_all() {
1676 let (_tmp, db) = setup();
1677 let (rows, count) = query(&db, "FROM blocks").unwrap();
1678 assert_eq!(count, 5);
1679 let _ = rows;
1680 }
1681
1682 #[test]
1683 fn where_eq() {
1684 let (_tmp, db) = setup();
1685 let (rows, count) = query(&db, r#"FROM blocks WHERE _id = "3""#).unwrap();
1686 assert_eq!(count, 1);
1687 assert_eq!(rows[0]["_id"], "3");
1688 }
1689
1690 #[test]
1691 fn order_by_limit() {
1692 let (_tmp, db) = setup();
1693 let (rows, count) = query(&db, "FROM blocks ORDER BY height ASC LIMIT 3").unwrap();
1694 assert_eq!(count, 3);
1695 assert_eq!(rows[0]["height"], 1);
1696 assert_eq!(rows[2]["height"], 3);
1697 }
1698
1699 #[test]
1700 fn order_by_desc() {
1701 let (_tmp, db) = setup();
1702 let (rows, _) = query(&db, "FROM blocks ORDER BY height DESC LIMIT 2").unwrap();
1703 assert_eq!(rows[0]["height"], 5);
1704 }
1705
1706 #[test]
1707 fn where_gt() {
1708 let (_tmp, db) = setup();
1709 let (rows, _) = query(&db, "FROM blocks WHERE height > 3").unwrap();
1710 assert_eq!(rows.len(), 2);
1711 }
1712
1713 #[test]
1720 fn where_order_limit_does_not_truncate_before_filter() {
1721 let (_tmp, db) = setup();
1722 let (rows, count) =
1723 query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height LIMIT 2").unwrap();
1724 assert_eq!(count, 2, "both matching rows must survive the limit");
1725 let heights: Vec<u64> = rows.iter()
1726 .filter_map(|r| r["height"].as_u64())
1727 .collect();
1728 assert_eq!(heights, vec![4, 5]);
1729 let (rows_d, _) =
1731 query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height DESC LIMIT 1").unwrap();
1732 assert_eq!(rows_d.len(), 1);
1733 assert_eq!(rows_d[0]["height"], 5);
1734 }
1735
1736 fn setup_text() -> (tempfile::TempDir, Db) {
1744 let dir = tempdir().unwrap();
1745 let db = Db::open(dir.path(), None).unwrap();
1746 let rows = [
1747 ("1", serde_json::json!({"status": "open", "miner": "Acme Pool", "fee": 10})),
1748 ("2", serde_json::json!({"status": "pending", "miner": "acme solo", "fee": 20})),
1749 ("3", serde_json::json!({"status": "closed", "miner": "Zenith", "fee": 30})),
1750 ("4", serde_json::json!({"status": "open", "fee": 40})),
1751 ("5", serde_json::json!({"status": "voided", "miner": Value::Null, "fee": 50})),
1752 ];
1753 for (id, data) in rows {
1754 db.put("jobs", id, data, vec![], None, None).unwrap();
1755 }
1756 (dir, db)
1757 }
1758
1759 fn ids(rows: &[Value]) -> Vec<String> {
1760 let mut v: Vec<String> = rows.iter()
1761 .filter_map(|r| r["_id"].as_str().map(String::from))
1762 .collect();
1763 v.sort();
1764 v
1765 }
1766
1767 #[test]
1768 fn where_in_list() {
1769 let (_tmp, db) = setup();
1770 let (rows, _) = query(&db, "FROM blocks WHERE height IN (2, 4)").unwrap();
1771 assert_eq!(ids(&rows), vec!["2", "4"]);
1772 }
1773
1774 #[test]
1775 fn where_in_strings() {
1776 let (_tmp, db) = setup_text();
1777 let (rows, _) = query(&db, r#"FROM jobs WHERE status IN ("open", "closed")"#).unwrap();
1778 assert_eq!(ids(&rows), vec!["1", "3", "4"]);
1779 }
1780
1781 #[test]
1782 fn where_not_in() {
1783 let (_tmp, db) = setup();
1784 let (rows, _) = query(&db, "FROM blocks WHERE height NOT IN (1, 2, 3)").unwrap();
1785 assert_eq!(ids(&rows), vec!["4", "5"]);
1786 }
1787
1788 #[test]
1789 fn where_in_single_value_equals_eq() {
1790 let (_tmp, db) = setup();
1791 let (a, _) = query(&db, "FROM blocks WHERE height IN (3)").unwrap();
1792 let (b, _) = query(&db, "FROM blocks WHERE height = 3").unwrap();
1793 assert_eq!(ids(&a), ids(&b));
1794 }
1795
1796 #[test]
1797 fn where_between_is_inclusive() {
1798 let (_tmp, db) = setup();
1799 let (rows, _) = query(&db, "FROM blocks WHERE height BETWEEN 2 AND 4").unwrap();
1800 assert_eq!(ids(&rows), vec!["2", "3", "4"]);
1802 }
1803
1804 #[test]
1805 fn where_not_between() {
1806 let (_tmp, db) = setup();
1807 let (rows, _) = query(&db, "FROM blocks WHERE height NOT BETWEEN 2 AND 4").unwrap();
1808 assert_eq!(ids(&rows), vec!["1", "5"]);
1809 }
1810
1811 #[test]
1815 fn between_and_does_not_swallow_the_conjunction() {
1816 let (_tmp, db) = setup();
1817 let (rows, _) = query(
1818 &db, "FROM blocks WHERE height BETWEEN 2 AND 4 AND n_tx > 4").unwrap();
1819 assert_eq!(ids(&rows), vec!["3", "4"]);
1821 }
1822
1823 #[test]
1824 fn where_like_prefix_suffix_and_infix() {
1825 let (_tmp, db) = setup_text();
1826 let (pre, _) = query(&db, r#"FROM jobs WHERE miner LIKE "Acme%""#).unwrap();
1827 assert_eq!(ids(&pre), vec!["1"]);
1828 let (suf, _) = query(&db, r#"FROM jobs WHERE miner LIKE "%Pool""#).unwrap();
1829 assert_eq!(ids(&suf), vec!["1"]);
1830 let (inf, _) = query(&db, r#"FROM jobs WHERE status LIKE "%pen%""#).unwrap();
1831 assert_eq!(ids(&inf), vec!["1", "2", "4"]); }
1833
1834 #[test]
1835 fn where_like_underscore_matches_exactly_one_char() {
1836 let (_tmp, db) = setup_text();
1837 let (rows, _) = query(&db, r#"FROM jobs WHERE status LIKE "open_""#).unwrap();
1838 assert!(rows.is_empty(), "`open_` must not match the 4-char value `open`");
1839 let (rows2, _) = query(&db, r#"FROM jobs WHERE status LIKE "ope_""#).unwrap();
1840 assert_eq!(ids(&rows2), vec!["1", "4"]);
1841 }
1842
1843 #[test]
1844 fn where_ilike_is_case_insensitive_and_like_is_not() {
1845 let (_tmp, db) = setup_text();
1846 let (ci, _) = query(&db, r#"FROM jobs WHERE miner ILIKE "acme%""#).unwrap();
1847 assert_eq!(ci.len(), 2, "ILIKE matches both `Acme Pool` and `acme solo`");
1848 let (cs, _) = query(&db, r#"FROM jobs WHERE miner LIKE "acme%""#).unwrap();
1849 assert_eq!(ids(&cs), vec!["2"], "LIKE stays case-sensitive");
1850 }
1851
1852 #[test]
1855 fn like_backtracks_across_multiple_wildcards() {
1856 assert!(like_match("abcabcabd", "%abc%abd", false));
1857 assert!(like_match("aaa", "%a", false));
1858 assert!(like_match("", "%", false));
1859 assert!(like_match("x", "%%%", false));
1860 assert!(!like_match("abc", "%abd", false));
1861 assert!(!like_match("ab", "ab_", false));
1862 assert!(like_match("héllo wörld", "h_llo w%d", false));
1863 }
1864
1865 #[test]
1868 fn regex_anchors_behave_as_posix_says() {
1869 assert!(regex_match("pg_catalog", "^pg_", false));
1871 assert!(regex_match("pg_toast_1", "^pg_toast", false));
1872 assert!(!regex_match("public", "^pg_", false));
1873 assert!(!regex_match("my_pg_stuff", "^pg_", false));
1876
1877 assert!(regex_match("report.sql", "sql$", false));
1878 assert!(!regex_match("sql_report", "sql$", false));
1879 assert!(regex_match("public", "^public$", false));
1881 assert!(!regex_match("public2", "^public$", false));
1882 assert!(regex_match("xxpg_yy", "pg_", false));
1884 assert!(!regex_match("xxqg_yy", "pg_", false));
1885 }
1886
1887 #[test]
1888 fn regex_dot_matches_exactly_one_character() {
1889 assert!(regex_match("abc", "a.c", false));
1890 assert!(!regex_match("ac", "a.c", false), "`.` is one char, not zero");
1891 assert!(!regex_match("abbc", "a.c", false), "`.` is one char, not many");
1892 assert!(regex_match("héllo", "h.llo", false));
1894 }
1895
1896 #[test]
1897 fn regex_case_insensitivity_is_opt_in() {
1898 assert!(regex_match("PG_CATALOG", "^pg_", true));
1899 assert!(!regex_match("PG_CATALOG", "^pg_", false),
1900 "`~` is case SENSITIVE; only `~*` folds case");
1901 }
1902
1903 #[test]
1904 fn an_empty_regex_matches_anything() {
1905 assert!(regex_match("anything", "", false));
1907 assert!(regex_match("", "", false));
1908 assert!(regex_match("x", "$", false));
1909 }
1910
1911 #[test]
1912 fn an_unsupported_regex_metacharacter_is_REFUSED_not_approximated() {
1913 for pat in ["a+b", "a*b", "a?b", "[ab]", "(a|b)", "a{2}", "a\\.b"] {
1917 assert!(unsupported_regex_char(pat).is_some(),
1918 "{:?} must be refused, not matched approximately", pat);
1919 }
1920 for pat in ["^pg_", "sql$", "^public$", "a.c", "plain", ""] {
1921 assert_eq!(unsupported_regex_char(pat), None, "{:?} is in the subset", pat);
1922 }
1923 assert_eq!(unsupported_regex_char("a\\.b"), Some('\\'));
1927 }
1928
1929 #[test]
1930 fn the_regex_operators_parse_in_all_four_spellings() {
1931 for (nql, negated, ci) in [
1932 (r#"FROM t WHERE nspname ~ "^pg_""#, false, false),
1933 (r#"FROM t WHERE nspname ~* "^pg_""#, false, true),
1934 (r#"FROM t WHERE nspname !~ "^pg_""#, true, false),
1935 (r#"FROM t WHERE nspname !~* "^pg_""#, true, true),
1936 ] {
1937 let q = parse(nql).unwrap_or_else(|e| panic!("{}: {}", nql, e));
1938 match q.where_.expect("a predicate") {
1939 Pred::Regex { field, pattern, negated: n, ci: c } => {
1940 assert_eq!(field, "nspname");
1941 assert_eq!(pattern, "^pg_");
1942 assert_eq!((n, c), (negated, ci), "{}", nql);
1943 }
1944 other => panic!("{} parsed as {:?}", nql, other),
1945 }
1946 }
1947 }
1948
1949 #[test]
1950 fn a_bad_regex_is_rejected_at_parse_time_with_the_offending_char() {
1951 let e = parse(r#"FROM t WHERE x ~ "a+b""#).unwrap_err().to_string();
1952 assert!(e.contains('+'), "the error must name the character: {}", e);
1953 assert!(e.contains("refused"), "{}", e);
1954 }
1955
1956 #[test]
1957 fn a_regex_over_a_missing_field_is_false_in_both_polarities() {
1958 let (_tmp, db) = setup_items();
1961 let (m, _) = query(&db, r#"FROM items WHERE nosuchfield ~ "x""#).unwrap();
1962 assert!(m.is_empty());
1963 let (n, _) = query(&db, r#"FROM items WHERE nosuchfield !~ "x""#).unwrap();
1964 assert!(n.is_empty(), "NOT over NULL must not match either");
1965 }
1966
1967 #[test]
1968 fn the_regex_operator_works_end_to_end_over_stored_documents() {
1969 let (_tmp, db) = setup_items();
1970 let (all, _) = query(&db, "FROM items").unwrap();
1971 let want: Vec<String> = all.iter()
1972 .filter_map(|r| r["_id"].as_str().map(str::to_string)).collect();
1973 let (got, _) = query(&db, r#"FROM items WHERE _id ~ """#).unwrap();
1976 assert_eq!(got.len(), want.len());
1977 let (none, _) = query(&db, r#"FROM items WHERE _id !~ """#).unwrap();
1979 assert!(none.is_empty());
1980 }
1981
1982 #[test]
1983 fn where_not_like() {
1984 let (_tmp, db) = setup_text();
1985 let (rows, _) = query(&db, r#"FROM jobs WHERE status NOT LIKE "open""#).unwrap();
1986 assert_eq!(ids(&rows), vec!["2", "3", "5"]);
1987 }
1988
1989 #[test]
1993 fn like_over_null_is_false_in_both_polarities() {
1994 let (_tmp, db) = setup_text();
1995 let (pos, _) = query(&db, r#"FROM jobs WHERE miner LIKE "%""#).unwrap();
1996 let (neg, _) = query(&db, r#"FROM jobs WHERE miner NOT LIKE "%""#).unwrap();
1997 assert!(!ids(&pos).contains(&"4".to_string()));
1998 assert!(!ids(&neg).contains(&"4".to_string()));
1999 assert!(!ids(&pos).contains(&"5".to_string()));
2000 assert!(!ids(&neg).contains(&"5".to_string()));
2001 }
2002
2003 #[test]
2006 fn where_is_null_catches_absent_and_explicit_null() {
2007 let (_tmp, db) = setup_text();
2008 let (rows, _) = query(&db, "FROM jobs WHERE miner IS NULL").unwrap();
2009 assert_eq!(ids(&rows), vec!["4", "5"]);
2010 }
2011
2012 #[test]
2013 fn where_is_not_null() {
2014 let (_tmp, db) = setup_text();
2015 let (rows, _) = query(&db, "FROM jobs WHERE miner IS NOT NULL").unwrap();
2016 assert_eq!(ids(&rows), vec!["1", "2", "3"]);
2017 }
2018
2019 #[test]
2020 fn where_or() {
2021 let (_tmp, db) = setup();
2022 let (rows, _) = query(&db, "FROM blocks WHERE height = 1 OR height = 5").unwrap();
2023 assert_eq!(ids(&rows), vec!["1", "5"]);
2024 }
2025
2026 #[test]
2029 fn and_binds_tighter_than_or() {
2030 let (_tmp, db) = setup();
2031 let (rows, _) = query(
2032 &db, "FROM blocks WHERE height = 1 OR height = 5 AND n_tx = 10").unwrap();
2033 assert_eq!(ids(&rows), vec!["1", "5"]);
2034 let (rows2, _) = query(
2035 &db, "FROM blocks WHERE height = 1 OR height = 5 AND n_tx = 99").unwrap();
2036 assert_eq!(ids(&rows2), vec!["1"], "the AND arm must not match");
2037 }
2038
2039 #[test]
2041 fn parens_override_precedence() {
2042 let (_tmp, db) = setup();
2043 let (rows, _) = query(
2044 &db, "FROM blocks WHERE (height = 1 OR height = 5) AND n_tx = 10").unwrap();
2045 assert_eq!(ids(&rows), vec!["5"]);
2046 }
2047
2048 #[test]
2049 fn nested_parens() {
2050 let (_tmp, db) = setup();
2051 let (rows, _) = query(
2052 &db,
2053 "FROM blocks WHERE ((height >= 2 AND height <= 4) OR height = 1) AND n_tx != 6",
2054 ).unwrap();
2055 assert_eq!(ids(&rows), vec!["1", "2", "4"]);
2056 }
2057
2058 #[test]
2059 fn not_negates_a_group() {
2060 let (_tmp, db) = setup();
2061 let (rows, _) = query(&db, "FROM blocks WHERE NOT (height > 2)").unwrap();
2062 assert_eq!(ids(&rows), vec!["1", "2"]);
2063 }
2064
2065 #[test]
2069 fn prefix_not_before_a_comparison() {
2070 let (_tmp, db) = setup();
2071 let (rows, _) = query(&db, "FROM blocks WHERE NOT height = 1").unwrap();
2072 assert_eq!(ids(&rows), vec!["2", "3", "4", "5"]);
2073 let (double, _) = query(&db, "FROM blocks WHERE NOT NOT height = 1").unwrap();
2074 assert_eq!(ids(&double), vec!["1"]);
2075 let (mixed, _) = query(&db, "FROM blocks WHERE NOT height = 1 AND height < 4").unwrap();
2076 assert_eq!(ids(&mixed), vec!["2", "3"]);
2077 }
2078
2079 #[test]
2083 fn id_equality_under_or_does_not_become_a_point_lookup() {
2084 let (_tmp, db) = setup();
2085 let (rows, _) = query(&db, r#"FROM blocks WHERE _id = "1" OR height > 3"#).unwrap();
2086 assert_eq!(ids(&rows), vec!["1", "4", "5"],
2087 "the OR arm must survive the id fast path");
2088 }
2089
2090 #[test]
2092 fn id_equality_under_and_still_point_looks_up() {
2093 let (_tmp, db) = setup();
2094 let (hit, _) = query(&db, r#"FROM blocks WHERE _id = "3" AND n_tx = 6"#).unwrap();
2095 assert_eq!(ids(&hit), vec!["3"]);
2096 let (miss, _) = query(&db, r#"FROM blocks WHERE _id = "3" AND n_tx = 999"#).unwrap();
2097 assert!(miss.is_empty(), "the second conjunct must still be applied");
2098 }
2099
2100 #[test]
2101 fn metadata_fields_are_filterable() {
2102 let (_tmp, db) = setup();
2103 let (rows, _) = query(&db, "FROM blocks WHERE _seq >= 0 AND _coll = blocks").unwrap();
2105 assert_eq!(rows.len(), 5);
2106 let (tail, _) = query(&db, "FROM blocks WHERE _seq > 0").unwrap();
2107 assert_eq!(tail.len(), 4);
2108 }
2109
2110 #[test]
2111 fn combined_with_order_and_limit() {
2112 let (_tmp, db) = setup();
2113 let (rows, _) = query(
2114 &db,
2115 "FROM blocks WHERE height IN (1, 3, 5) ORDER BY height DESC LIMIT 2",
2116 ).unwrap();
2117 let heights: Vec<u64> = rows.iter().filter_map(|r| r["height"].as_u64()).collect();
2118 assert_eq!(heights, vec![5, 3]);
2119 }
2120
2121 #[test]
2127 fn unknown_clauses_are_errors_not_silent_skips() {
2128 let (_tmp, db) = setup();
2129 for bad in [
2130 "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,", ] {
2138 assert!(query(&db, bad).is_err(), "`{}` must be rejected, not silently reinterpreted", bad);
2139 }
2140 }
2141
2142 #[test]
2143 fn malformed_predicates_are_errors() {
2144 let (_tmp, db) = setup();
2145 for bad in [
2146 "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", ] {
2156 assert!(query(&db, bad).is_err(), "`{}` must be a parse error", bad);
2157 }
2158 }
2159
2160 #[test]
2164 fn asc_is_accepted_explicitly() {
2165 let (_tmp, db) = setup();
2166 let (asc, _) = query(&db, "FROM blocks ORDER BY height ASC").unwrap();
2167 let (plain, _) = query(&db, "FROM blocks ORDER BY height").unwrap();
2168 assert_eq!(asc[0]["height"], 1);
2169 assert_eq!(plain[0]["height"], 1);
2170 }
2171
2172 #[test]
2175 fn new_keywords_are_case_insensitive() {
2176 let (_tmp, db) = setup();
2177 let (rows, _) = query(&db, "from blocks where height between 2 and 3").unwrap();
2178 assert_eq!(ids(&rows), vec!["2", "3"]);
2179 let (rows2, _) = query(&db, "FROM blocks Where height In (1) Or height In (2)").unwrap();
2180 assert_eq!(ids(&rows2), vec!["1", "2"]);
2181 }
2182
2183 #[test]
2184 fn group_by_count() {
2185 let (_tmp, db) = setup();
2186 let (rows, _) = query(&db, "FROM blocks GROUP BY n_tx COUNT").unwrap();
2187 assert_eq!(rows.len(), 5); }
2189
2190 fn twin(fields: &[&str]) -> (tempfile::TempDir, tempfile::TempDir, Db, Db) {
2202 let d1 = tempdir().unwrap();
2203 let d2 = tempdir().unwrap();
2204 let indexed = Db::open(d1.path(), None).unwrap();
2205 let plain = Db::open(d2.path(), None).unwrap();
2206 for f in fields {
2207 indexed.create_sorted_index("t", f);
2208 }
2209 let rows: Vec<(String, Value)> = (0..40u64).map(|i| {
2212 let mut o = serde_json::Map::new();
2213 if i % 7 != 0 {
2214 o.insert("fee".into(), json!(i % 13));
2215 }
2216 if i % 11 == 0 {
2217 o.insert("note".into(), Value::Null);
2218 } else {
2219 o.insert("note".into(), json!(format!("n{}", i % 5)));
2220 }
2221 o.insert("rank".into(), json!(40 - i));
2222 (i.to_string(), Value::Object(o))
2223 }).collect();
2224 for (id, doc) in &rows {
2225 indexed.put("t", id, doc.clone(), vec![], None, None).unwrap();
2226 plain.put("t", id, doc.clone(), vec![], None, None).unwrap();
2227 }
2228 (d1, d2, indexed, plain)
2229 }
2230
2231 fn same(a: &Db, b: &Db, nql: &str) -> (Vec<String>, Vec<String>) {
2232 let ga = {
2233 let (rows, _) = query(a, nql).unwrap();
2234 let mut v: Vec<String> = rows.iter()
2235 .filter_map(|r| r["_id"].as_str().map(String::from)).collect();
2236 v.sort(); v
2237 };
2238 let gb = {
2239 let (rows, _) = query(b, nql).unwrap();
2240 let mut v: Vec<String> = rows.iter()
2241 .filter_map(|r| r["_id"].as_str().map(String::from)).collect();
2242 v.sort(); v
2243 };
2244 (ga, gb)
2245 }
2246
2247 #[test]
2248 fn indexed_and_unindexed_agree_on_every_predicate_shape() {
2249 let (_t1, _t2, idx, plain) = twin(&["fee", "note", "rank"]);
2250 for nql in [
2251 "FROM t WHERE fee > 5",
2253 "FROM t WHERE fee >= 5",
2254 "FROM t WHERE fee < 5",
2255 "FROM t WHERE fee <= 5",
2256 "FROM t WHERE fee = 5",
2257 "FROM t WHERE fee BETWEEN 3 AND 8",
2258 "FROM t WHERE fee NOT BETWEEN 3 AND 8",
2259 "FROM t WHERE fee IN (1, 5, 9)",
2260 "FROM t WHERE fee NOT IN (1, 5, 9)",
2261 "FROM t WHERE fee != 5",
2262 "FROM t WHERE fee > 3 AND fee < 9",
2264 "FROM t WHERE fee >= 3 AND fee <= 9",
2265 "FROM t WHERE fee > 3 AND fee < 9 AND fee != 5",
2266 "FROM t WHERE fee BETWEEN 2 AND 10 AND fee > 6",
2267 "FROM t WHERE fee > 5 AND rank < 20",
2269 "FROM t WHERE fee IN (2, 3) AND rank > 10",
2270 "FROM t WHERE fee = 4 AND rank = 8",
2271 "FROM t WHERE fee IS NULL",
2273 "FROM t WHERE fee IS NOT NULL",
2274 "FROM t WHERE note IS NULL",
2275 "FROM t WHERE note IS NOT NULL",
2276 "FROM t WHERE fee IS NULL AND rank > 20",
2277 r#"FROM t WHERE note LIKE "n_""#,
2279 r#"FROM t WHERE fee > 5 AND note LIKE "n1""#,
2280 r#"FROM t WHERE note NOT LIKE "n1" AND fee < 4"#,
2281 "FROM t WHERE fee > 11 OR rank > 38",
2283 "FROM t WHERE fee = 1 OR note IS NULL",
2284 "FROM t WHERE (fee > 11 OR rank > 38) AND rank < 39",
2285 "FROM t WHERE fee IN (1) OR fee IN (2)",
2286 "FROM t WHERE NOT (fee > 5)",
2288 "FROM t WHERE NOT (fee IN (1, 2))",
2289 "FROM t WHERE NOT (fee > 5) AND rank < 30",
2290 "FROM t WHERE fee > 5 ORDER BY rank DESC LIMIT 5",
2292 "FROM t WHERE fee BETWEEN 2 AND 8 ORDER BY fee, rank DESC",
2293 "FROM t WHERE fee > 5 GROUP BY note COUNT",
2294 "FROM t WHERE fee > 5 COUNT",
2295 "FROM t WHERE fee > 5 ORDER BY rank LIMIT 3 OFFSET 2",
2296 "FROM t WHERE fee > 9999",
2298 "FROM t WHERE fee IN (9999)",
2299 "FROM t WHERE fee BETWEEN 100 AND 200",
2300 ] {
2301 let (a, b) = same(&idx, &plain, nql);
2302 assert_eq!(a, b, "indexed and unindexed disagree on `{}`", nql);
2303 }
2304 }
2305
2306 #[test]
2310 fn index_path_still_honours_order_by() {
2311 let (_t1, _t2, idx, plain) = twin(&["fee", "rank"]);
2312 for nql in [
2313 "FROM t WHERE fee > 4 ORDER BY rank",
2314 "FROM t WHERE fee > 4 ORDER BY rank DESC",
2315 "FROM t WHERE fee > 4 ORDER BY note, rank DESC",
2316 "FROM t WHERE fee BETWEEN 2 AND 9 ORDER BY rank LIMIT 4",
2317 "FROM t WHERE fee IN (3, 6) ORDER BY rank DESC LIMIT 2",
2318 ] {
2319 let ra = query(&idx, nql).unwrap().0;
2320 let rb = query(&plain, nql).unwrap().0;
2321 let ia: Vec<&str> = ra.iter().filter_map(|r| r["_id"].as_str()).collect();
2322 let ib: Vec<&str> = rb.iter().filter_map(|r| r["_id"].as_str()).collect();
2323 assert_eq!(ia, ib, "row ORDER differs on `{}`", nql);
2324 }
2325 }
2326
2327 #[test]
2336 fn an_ordering_comparison_against_a_missing_field_is_false() {
2337 let dir = tempdir().unwrap();
2338 let db = Db::open(dir.path(), None).unwrap();
2339 db.put("t", "has", json!({"fee": 1}), vec![], None, None).unwrap();
2340 db.put("t", "none", json!({"other": 1}), vec![], None, None).unwrap();
2341 db.put("t", "null", json!({"fee": Value::Null}), vec![], None, None).unwrap();
2342
2343 for nql in ["FROM t WHERE fee < 5", "FROM t WHERE fee <= 5"] {
2344 let (r, _) = query(&db, nql).unwrap();
2345 let ids: Vec<&str> = r.iter().filter_map(|x| x["_id"].as_str()).collect();
2346 assert_eq!(ids, vec!["has"],
2347 "`{}` must not match a row whose fee is absent or null", nql);
2348 }
2349 for nql in ["FROM t WHERE fee > 0", "FROM t WHERE fee >= 0"] {
2350 let (r, _) = query(&db, nql).unwrap();
2351 let ids: Vec<&str> = r.iter().filter_map(|x| x["_id"].as_str()).collect();
2352 assert_eq!(ids, vec!["has"], "`{}`", nql);
2353 }
2354 let (b, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 9").unwrap();
2356 assert_eq!(b.len(), 1);
2357
2358 let (ne, _) = query(&db, "FROM t WHERE fee != 5").unwrap();
2361 assert_eq!(ne.len(), 3, "!= still matches absent and null fields");
2362 let (isnull, _) = query(&db, "FROM t WHERE fee = NULL").unwrap();
2363 assert_eq!(isnull.len(), 2, "absent and explicit-null both equal NULL");
2364
2365 let d2 = tempdir().unwrap();
2368 let idx = Db::open(d2.path(), None).unwrap();
2369 idx.create_sorted_index("t", "fee");
2370 idx.put("t", "has", json!({"fee": 1}), vec![], None, None).unwrap();
2371 idx.put("t", "none", json!({"other": 1}), vec![], None, None).unwrap();
2372 idx.put("t", "null", json!({"fee": Value::Null}), vec![], None, None).unwrap();
2373 for nql in ["FROM t WHERE fee < 5", "FROM t WHERE fee <= 5",
2374 "FROM t WHERE fee > 0", "FROM t WHERE fee BETWEEN 0 AND 9"] {
2375 let (a, _) = query(&db, nql).unwrap();
2376 let (b, _) = query(&idx, nql).unwrap();
2377 let ia: Vec<&str> = a.iter().filter_map(|x| x["_id"].as_str()).collect();
2378 let ib: Vec<&str> = b.iter().filter_map(|x| x["_id"].as_str()).collect();
2379 assert_eq!(ia, ib, "indexed and unindexed disagree on `{}`", nql);
2380 }
2381 }
2382
2383 #[test]
2388 fn is_null_never_uses_the_index() {
2389 let (_t1, _t2, idx, plain) = twin(&["fee"]);
2390 let (a, b) = same(&idx, &plain, "FROM t WHERE fee IS NULL");
2391 assert_eq!(a, b);
2392 assert_eq!(a, vec!["0", "14", "21", "28", "35", "7"]);
2394 assert!(!a.is_empty(), "the fixture must actually contain absent fields");
2395 }
2396
2397 #[test]
2400 fn a_disjunct_is_never_used_to_narrow() {
2401 let (_t1, _t2, idx, plain) = twin(&["fee", "rank"]);
2402 let nql = "FROM t WHERE fee = 1 OR rank = 40";
2403 let (a, b) = same(&idx, &plain, nql);
2404 assert_eq!(a, b);
2405 assert!(a.contains(&"0".to_string()),
2408 "the OR arm matching a doc with no indexed field must survive: {:?}", a);
2409 assert!(a.len() > 1, "both arms must contribute: {:?}", a);
2410 }
2411
2412 #[test]
2416 fn as_of_does_not_use_the_current_version_index() {
2417 let dir = tempdir().unwrap();
2418 let db = Db::open(dir.path(), None).unwrap();
2419 db.create_sorted_index("t", "fee");
2420 db.put("t", "a", json!({"fee": 5}), vec![], None, None).unwrap();
2421 let snap = db.put("t", "b", json!({"fee": 5}), vec![], None, None).unwrap().seq;
2422 db.put("t", "a", json!({"fee": 999}), vec![], None, None).unwrap();
2424 db.put("t", "b", json!({"fee": 999}), vec![], None, None).unwrap();
2425
2426 let (now, _) = query(&db, "FROM t WHERE fee = 5").unwrap();
2428 assert!(now.is_empty(), "current versions have fee 999: {:?}", now);
2429
2430 let (then, _) = query(&db, &format!("FROM t AS OF {} WHERE fee = 5", snap)).unwrap();
2433 let mut ids: Vec<&str> = then.iter().filter_map(|r| r["_id"].as_str()).collect();
2434 ids.sort();
2435 assert_eq!(ids, vec!["a", "b"], "AS OF must see the historical values");
2436
2437 let (r, _) = query(&db, &format!("FROM t AS OF {} WHERE fee BETWEEN 1 AND 9", snap)).unwrap();
2439 assert_eq!(r.len(), 2);
2440 let (i, _) = query(&db, &format!("FROM t AS OF {} WHERE fee IN (5)", snap)).unwrap();
2441 assert_eq!(i.len(), 2);
2442 }
2443
2444 #[test]
2446 fn the_index_path_returns_current_versions_only() {
2447 let dir = tempdir().unwrap();
2448 let db = Db::open(dir.path(), None).unwrap();
2449 db.create_sorted_index("t", "fee");
2450 for i in 0..5u64 {
2451 db.put("t", &i.to_string(), json!({"fee": i}), vec![], None, None).unwrap();
2452 }
2453 db.put("t", "0", json!({"fee": 100}), vec![], None, None).unwrap();
2454
2455 let (low, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 4").unwrap();
2456 let mut ids: Vec<&str> = low.iter().filter_map(|r| r["_id"].as_str()).collect();
2457 ids.sort();
2458 assert_eq!(ids, vec!["1", "2", "3", "4"],
2459 "doc 0 moved to fee 100 and must not appear in 0..4");
2460
2461 let (high, _) = query(&db, "FROM t WHERE fee = 100").unwrap();
2462 assert_eq!(high.len(), 1);
2463 assert_eq!(high[0]["_id"], "0");
2464 assert_eq!(high[0]["fee"], json!(100), "the CURRENT value, not the old one");
2465 }
2466
2467 #[test]
2470 fn index_scans_do_not_duplicate_rows() {
2471 let dir = tempdir().unwrap();
2472 let db = Db::open(dir.path(), None).unwrap();
2473 db.create_sorted_index("t", "fee");
2474 for i in 0..6u64 {
2475 db.put("t", &i.to_string(), json!({"fee": i % 2}), vec![], None, None).unwrap();
2476 }
2477 let (dup, _) = query(&db, "FROM t WHERE fee IN (0, 0, 1, 1)").unwrap();
2478 assert_eq!(dup.len(), 6, "each row once despite repeated IN arms");
2479 let (r, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 1").unwrap();
2480 assert_eq!(r.len(), 6);
2481 let mut ids: Vec<&str> = dup.iter().filter_map(|r| r["_id"].as_str()).collect();
2482 ids.sort();
2483 ids.dedup();
2484 assert_eq!(ids.len(), 6, "no duplicate _ids");
2485 }
2486
2487 #[test]
2489 fn index_ranges_work_on_strings() {
2490 let dir = tempdir().unwrap();
2491 let db = Db::open(dir.path(), None).unwrap();
2492 db.create_sorted_index("t", "name");
2493 for (i, n) in ["alpha", "bravo", "charlie", "delta", "echo"].iter().enumerate() {
2494 db.put("t", &i.to_string(), json!({"name": n}), vec![], None, None).unwrap();
2495 }
2496 let (r, _) = query(&db, r#"FROM t WHERE name BETWEEN "bravo" AND "delta""#).unwrap();
2497 let mut got: Vec<&str> = r.iter().filter_map(|x| x["name"].as_str()).collect();
2498 got.sort();
2499 assert_eq!(got, vec!["bravo", "charlie", "delta"]);
2500 let (gt, _) = query(&db, r#"FROM t WHERE name > "charlie""#).unwrap();
2501 assert_eq!(gt.len(), 2);
2502 }
2503
2504 #[test]
2507 fn same_field_bounds_are_merged_tightest_wins() {
2508 let (_t1, _t2, idx, plain) = twin(&["fee"]);
2509 for (a_nql, b_nql) in [
2510 ("FROM t WHERE fee > 2 AND fee > 6", "FROM t WHERE fee > 6"),
2511 ("FROM t WHERE fee > 6 AND fee > 2", "FROM t WHERE fee > 6"),
2512 ("FROM t WHERE fee < 9 AND fee < 4", "FROM t WHERE fee < 4"),
2513 ("FROM t WHERE fee BETWEEN 0 AND 12 AND fee >= 5 AND fee <= 7",
2514 "FROM t WHERE fee >= 5 AND fee <= 7"),
2515 ] {
2516 let (ia, _) = same(&idx, &plain, a_nql);
2517 let (ib, _) = same(&idx, &plain, b_nql);
2518 assert_eq!(ia, ib, "`{}` should equal `{}`", a_nql, b_nql);
2519 }
2520 }
2521
2522 #[test]
2525 fn a_predicate_on_an_unindexed_field_still_answers() {
2526 let (_t1, _t2, idx, plain) = twin(&["fee"]); for nql in [
2528 "FROM t WHERE rank > 30",
2529 "FROM t WHERE rank BETWEEN 10 AND 20",
2530 "FROM t WHERE rank IN (40, 39)",
2531 "FROM t WHERE rank > 30 AND fee > 2",
2532 ] {
2533 let (a, b) = same(&idx, &plain, nql);
2534 assert_eq!(a, b, "`{}`", nql);
2535 }
2536 }
2537
2538 #[test]
2541 fn range_cardinality_counts_without_reading() {
2542 let dir = tempdir().unwrap();
2543 let db = Db::open(dir.path(), None).unwrap();
2544 db.create_sorted_index("t", "fee");
2545 for i in 0..20u64 {
2546 db.put("t", &i.to_string(), json!({"fee": i}), vec![], None, None).unwrap();
2547 }
2548 assert_eq!(db.range_cardinality("t", "fee", None, None, true, true), Some(20));
2549 assert_eq!(
2550 db.range_cardinality("t", "fee", Some(&json!(5)), Some(&json!(9)), true, true),
2551 Some(5), "5..=9 inclusive is five values");
2552 assert_eq!(
2553 db.range_cardinality("t", "fee", Some(&json!(5)), Some(&json!(9)), false, false),
2554 Some(3), "exclusive bounds drop both ends");
2555 assert_eq!(
2556 db.range_cardinality("t", "fee", Some(&json!(18)), None, true, true),
2557 Some(2));
2558 assert_eq!(
2559 db.range_cardinality("t", "fee", Some(&json!(999)), None, true, true),
2560 Some(0), "an empty range is 0, not an error");
2561 assert_eq!(db.range_cardinality("t", "nope", None, None, true, true), None);
2563 }
2564
2565 #[test]
2569 fn the_narrower_index_is_preferred() {
2570 let dir = tempdir().unwrap();
2571 let db = Db::open(dir.path(), None).unwrap();
2572 db.create_sorted_index("t", "wide");
2573 db.create_sorted_index("t", "narrow");
2574 for i in 0..100u64 {
2575 db.put("t", &i.to_string(),
2576 json!({"wide": i % 2, "narrow": i}), vec![], None, None).unwrap();
2577 }
2578 let wide = db.range_cardinality("t", "wide", Some(&json!(0)), Some(&json!(0)), true, true);
2580 let narrow = db.range_cardinality("t", "narrow", Some(&json!(7)), Some(&json!(7)), true, true);
2581 assert_eq!(wide, Some(50));
2582 assert_eq!(narrow, Some(1));
2583 let (r, _) = query(&db, "FROM t WHERE wide = 0 AND narrow = 7").unwrap();
2585 assert!(r.is_empty(), "narrow 7 has wide 1, so nothing matches");
2586 let (r2, _) = query(&db, "FROM t WHERE wide = 0 AND narrow = 8").unwrap();
2587 assert_eq!(r2.len(), 1);
2588 assert_eq!(r2[0]["_id"], "8");
2589 }
2590
2591 fn heights(rows: &[Value]) -> Vec<u64> {
2595 rows.iter().filter_map(|r| r["height"].as_u64()).collect()
2596 }
2597
2598 #[test]
2599 fn offset_skips_result_rows() {
2600 let (_tmp, db) = setup();
2601 let (rows, _) = query(&db, "FROM blocks ORDER BY height OFFSET 2").unwrap();
2602 assert_eq!(heights(&rows), vec![3, 4, 5]);
2603 }
2604
2605 #[test]
2606 fn offset_with_limit_pages() {
2607 let (_tmp, db) = setup();
2608 let mut seen = vec![];
2612 for page in 0..3 {
2613 let (rows, _) = query(
2614 &db,
2615 &format!("FROM blocks ORDER BY height LIMIT 2 OFFSET {}", page * 2),
2616 ).unwrap();
2617 seen.extend(heights(&rows));
2618 }
2619 assert_eq!(seen, vec![1, 2, 3, 4, 5]);
2620 }
2621
2622 #[test]
2623 fn offset_past_the_end_is_an_empty_page() {
2624 let (_tmp, db) = setup();
2625 let (rows, count) = query(&db, "FROM blocks OFFSET 99").unwrap();
2626 assert!(rows.is_empty());
2627 assert_eq!(count, 0);
2628 let (zero, _) = query(&db, "FROM blocks OFFSET 0").unwrap();
2629 assert_eq!(zero.len(), 5, "OFFSET 0 skips nothing");
2630 }
2631
2632 #[test]
2633 fn offset_applies_after_the_filter() {
2634 let (_tmp, db) = setup();
2635 let (rows, _) = query(
2638 &db, "FROM blocks WHERE n_tx >= 6 ORDER BY height OFFSET 1").unwrap();
2639 assert_eq!(heights(&rows), vec![4, 5]);
2640 }
2641
2642 #[test]
2643 fn order_by_multiple_keys() {
2644 let dir = tempdir().unwrap();
2645 let db = Db::open(dir.path(), None).unwrap();
2646 for (i, (s, f)) in [("open", 30), ("open", 10), ("closed", 20),
2649 ("open", 20), ("closed", 5)].iter().enumerate() {
2650 db.put("t", &i.to_string(),
2651 serde_json::json!({"status": s, "fee": f}), vec![], None, None).unwrap();
2652 }
2653 let (rows, _) = query(&db, "FROM t ORDER BY status, fee DESC").unwrap();
2654 let got: Vec<(String, u64)> = rows.iter()
2655 .map(|r| (r["status"].as_str().unwrap().to_string(), r["fee"].as_u64().unwrap()))
2656 .collect();
2657 assert_eq!(got, vec![
2658 ("closed".into(), 20), ("closed".into(), 5),
2659 ("open".into(), 30), ("open".into(), 20), ("open".into(), 10),
2660 ]);
2661 }
2662
2663 #[test]
2664 fn order_by_mixed_directions() {
2665 let dir = tempdir().unwrap();
2666 let db = Db::open(dir.path(), None).unwrap();
2667 for (i, (a, b)) in [(1, 1), (1, 2), (2, 1), (2, 2)].iter().enumerate() {
2668 db.put("t", &i.to_string(),
2669 serde_json::json!({"a": a, "b": b}), vec![], None, None).unwrap();
2670 }
2671 let (rows, _) = query(&db, "FROM t ORDER BY a DESC, b ASC").unwrap();
2672 let got: Vec<(u64, u64)> = rows.iter()
2673 .map(|r| (r["a"].as_u64().unwrap(), r["b"].as_u64().unwrap()))
2674 .collect();
2675 assert_eq!(got, vec![(2, 1), (2, 2), (1, 1), (1, 2)]);
2676 }
2677
2678 #[test]
2683 fn limit_applies_to_grouped_rows_not_to_the_input() {
2684 let dir = tempdir().unwrap();
2685 let db = Db::open(dir.path(), None).unwrap();
2686 for i in 0..12 {
2687 let status = ["open", "closed", "void"][i % 3];
2689 db.put("t", &i.to_string(),
2690 serde_json::json!({"status": status, "fee": i}),
2691 vec![], None, None).unwrap();
2692 }
2693 let (all, _) = query(&db, "FROM t GROUP BY status COUNT").unwrap();
2694 assert_eq!(all.len(), 3);
2695 let total: u64 = all.iter().filter_map(|r| r["count"].as_u64()).sum();
2696 assert_eq!(total, 12, "every input row must be counted");
2697
2698 let (limited, _) = query(&db, "FROM t GROUP BY status COUNT LIMIT 2").unwrap();
2701 assert_eq!(limited.len(), 2, "LIMIT caps the number of groups");
2702 for r in &limited {
2703 assert_eq!(r["count"], json!(4),
2704 "each group keeps its true count, got {:?}", r);
2705 }
2706 }
2707
2708 #[test]
2713 fn order_by_sorts_the_grouped_rows() {
2714 let dir = tempdir().unwrap();
2715 let db = Db::open(dir.path(), None).unwrap();
2716 for (i, s) in ["a", "b", "b", "b", "c", "c"].iter().enumerate() {
2718 db.put("t", &i.to_string(),
2719 serde_json::json!({"g": s, "n": i}), vec![], None, None).unwrap();
2720 }
2721 let (rows, _) = query(&db, "FROM t GROUP BY g COUNT ORDER BY count DESC").unwrap();
2722 let got: Vec<(String, u64)> = rows.iter()
2723 .map(|r| (r["g"].as_str().unwrap().to_string(), r["count"].as_u64().unwrap()))
2724 .collect();
2725 assert_eq!(got, vec![("b".into(), 3), ("c".into(), 2), ("a".into(), 1)]);
2726
2727 let (by_key, _) = query(&db, "FROM t GROUP BY g COUNT ORDER BY g DESC").unwrap();
2729 let keys: Vec<&str> = by_key.iter().map(|r| r["g"].as_str().unwrap()).collect();
2730 assert_eq!(keys, vec!["c", "b", "a"]);
2731 }
2732
2733 #[test]
2734 fn order_by_an_aggregate_key() {
2735 let (_tmp, db) = setup_items();
2736 let (rows, _) = query(
2737 &db, "FROM items GROUP BY cat SUM price ORDER BY sum_price DESC").unwrap();
2738 let cats: Vec<&str> = rows.iter().map(|r| r["cat"].as_str().unwrap()).collect();
2739 assert_eq!(cats, vec!["y", "x"], "y sums to 60, x to 15");
2740 }
2741
2742 #[test]
2743 fn offset_and_limit_page_grouped_rows() {
2744 let dir = tempdir().unwrap();
2745 let db = Db::open(dir.path(), None).unwrap();
2746 for i in 0..9 {
2747 db.put("t", &i.to_string(),
2748 serde_json::json!({"g": format!("g{}", i % 3)}), vec![], None, None).unwrap();
2749 }
2750 let (page, _) = query(
2751 &db, "FROM t GROUP BY g COUNT ORDER BY g LIMIT 1 OFFSET 1").unwrap();
2752 assert_eq!(page.len(), 1);
2753 assert_eq!(page[0]["g"], "g1");
2754 }
2755
2756 #[test]
2759 fn having_filters_groups_by_count() {
2760 let dir = tempdir().unwrap();
2761 let db = Db::open(dir.path(), None).unwrap();
2762 for (i, s) in ["a", "b", "b", "b", "c", "c"].iter().enumerate() {
2763 db.put("t", &i.to_string(),
2764 serde_json::json!({"g": s, "n": i}), vec![], None, None).unwrap();
2765 }
2766 let (rows, _) = query(&db, "FROM t GROUP BY g COUNT HAVING count > 1").unwrap();
2767 let mut keys: Vec<&str> = rows.iter().map(|r| r["g"].as_str().unwrap()).collect();
2768 keys.sort();
2769 assert_eq!(keys, vec!["b", "c"], "the single-row group `a` is filtered out");
2770 }
2771
2772 #[test]
2773 fn having_filters_on_the_aggregate_value() {
2774 let (_tmp, db) = setup_items();
2775 let (rows, _) = query(
2777 &db, "FROM items GROUP BY cat SUM price HAVING sum_price > 20").unwrap();
2778 assert_eq!(rows.len(), 1);
2779 assert_eq!(rows[0]["cat"], "y");
2780 }
2781
2782 #[test]
2785 fn having_supports_the_whole_predicate_surface() {
2786 let (_tmp, db) = setup_items();
2787 let (in_, _) = query(
2788 &db, r#"FROM items GROUP BY cat COUNT HAVING cat IN ("x")"#).unwrap();
2789 assert_eq!(in_.len(), 1);
2790 assert_eq!(in_[0]["cat"], "x");
2791
2792 let (btw, _) = query(
2793 &db, "FROM items GROUP BY cat SUM price HAVING sum_price BETWEEN 10 AND 20").unwrap();
2794 assert_eq!(btw.len(), 1);
2795 assert_eq!(btw[0]["cat"], "x");
2796
2797 let (like, _) = query(
2798 &db, r#"FROM items GROUP BY cat COUNT HAVING cat LIKE "y""#).unwrap();
2799 assert_eq!(like.len(), 1);
2800
2801 let (or_, _) = query(
2802 &db, "FROM items GROUP BY cat SUM price HAVING sum_price < 20 OR count = 3").unwrap();
2803 assert_eq!(or_.len(), 2);
2804 }
2805
2806 #[test]
2809 fn where_and_having_are_different_stages() {
2810 let (_tmp, db) = setup_items();
2811 let (w, _) = query(
2813 &db, "FROM items WHERE price > 10 GROUP BY cat SUM price").unwrap();
2814 let x = w.iter().find(|r| r["cat"] == "x");
2815 assert!(x.is_none(), "x's rows (0,5,10) are all filtered out by WHERE");
2816
2817 let (h, _) = query(
2819 &db, "FROM items GROUP BY cat SUM price HAVING sum_price > 10").unwrap();
2820 assert_eq!(h.len(), 2, "both groups sum above 10 when nothing is pre-filtered");
2821 }
2822
2823 #[test]
2824 fn having_without_an_aggregate_is_an_error() {
2825 let (_tmp, db) = setup();
2826 assert!(query(&db, "FROM blocks HAVING height > 3").is_err());
2830 }
2831
2832 #[test]
2838 fn bare_count_returns_one_row() {
2839 let (_tmp, db) = setup();
2840 let (rows, _) = query(&db, "FROM blocks COUNT").unwrap();
2841 assert_eq!(rows.len(), 1);
2842 assert_eq!(rows[0]["count"], json!(5));
2843 assert_eq!(rows[0]["value"], json!(5));
2844 }
2845
2846 #[test]
2847 fn bare_count_respects_the_filter() {
2848 let (_tmp, db) = setup();
2849 let (rows, _) = query(&db, "FROM blocks WHERE height > 3 COUNT").unwrap();
2850 assert_eq!(rows[0]["count"], json!(2));
2851 }
2852
2853 #[test]
2854 fn bare_sum_avg_min_max() {
2855 let (_tmp, db) = setup();
2856 let (s, _) = query(&db, "FROM blocks SUM n_tx").unwrap();
2858 assert_eq!(s[0]["sum_n_tx"], json!(30), "integer inputs give an integer sum");
2859 let (a, _) = query(&db, "FROM blocks AVG n_tx").unwrap();
2860 assert_eq!(a[0]["avg_n_tx"], json!(6.0));
2861 let (mn, _) = query(&db, "FROM blocks MIN n_tx").unwrap();
2862 assert_eq!(mn[0]["min_n_tx"], json!(2));
2863 let (mx, _) = query(&db, "FROM blocks MAX n_tx").unwrap();
2864 assert_eq!(mx[0]["max_n_tx"], json!(10));
2865 }
2866
2867 #[test]
2875 fn integer_aggregates_stay_integers_and_keep_full_precision() {
2876 let dir = tempdir().unwrap();
2877 let db = Db::open(dir.path(), None).unwrap();
2878 let big: [i64; 3] = [9_007_199_254_740_993, 9_007_199_254_740_995, 1];
2881 for (i, v) in big.iter().enumerate() {
2882 db.put("t", &i.to_string(), serde_json::json!({"v": v}),
2883 vec![], None, None).unwrap();
2884 }
2885 let (s, _) = query(&db, "FROM t SUM v").unwrap();
2886 assert_eq!(s[0]["sum_v"], json!(18_014_398_509_481_989i64),
2887 "exact i64 sum, not a rounded f64");
2888 assert!(s[0]["sum_v"].is_i64(), "must serialise as an integer");
2889
2890 let (mx, _) = query(&db, "FROM t MAX v").unwrap();
2891 assert_eq!(mx[0]["max_v"], json!(9_007_199_254_740_995i64));
2892 let (mn, _) = query(&db, "FROM t MIN v").unwrap();
2893 assert_eq!(mn[0]["min_v"], json!(1));
2894 }
2895
2896 #[test]
2899 fn a_single_float_makes_the_aggregate_fractional() {
2900 let dir = tempdir().unwrap();
2901 let db = Db::open(dir.path(), None).unwrap();
2902 db.put("t", "1", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
2903 db.put("t", "2", serde_json::json!({"v": 2.5}), vec![], None, None).unwrap();
2904 let (s, _) = query(&db, "FROM t SUM v").unwrap();
2905 assert_eq!(s[0]["sum_v"], json!(3.5));
2906 let (a, _) = query(&db, "FROM t AVG v").unwrap();
2908 assert_eq!(a[0]["avg_v"], json!(1.75));
2909 }
2910
2911 #[test]
2914 fn booleans_are_not_aggregated_as_numbers() {
2915 let dir = tempdir().unwrap();
2916 let db = Db::open(dir.path(), None).unwrap();
2917 db.put("t", "1", serde_json::json!({"v": true}), vec![], None, None).unwrap();
2918 db.put("t", "2", serde_json::json!({"v": 5}), vec![], None, None).unwrap();
2919 let (s, _) = query(&db, "FROM t SUM v").unwrap();
2920 assert_eq!(s[0]["sum_v"], json!(5), "the bool contributes nothing");
2921 assert_eq!(s[0]["count"], json!(2), "but it still counts toward the group");
2922 }
2923
2924 #[test]
2927 fn bare_count_of_nothing_is_zero_not_empty() {
2928 let (_tmp, db) = setup();
2929 let (rows, count) = query(&db, "FROM blocks WHERE height > 999 COUNT").unwrap();
2930 assert_eq!(count, 1, "still exactly one row");
2931 assert_eq!(rows[0]["count"], json!(0));
2932
2933 let (g, _) = query(&db, "FROM blocks WHERE height > 999 GROUP BY height COUNT").unwrap();
2935 assert!(g.is_empty());
2936 }
2937
2938 #[test]
2939 fn bare_aggregate_over_an_empty_collection() {
2940 let (_tmp, db) = setup();
2941 let (rows, _) = query(&db, "FROM nonexistent COUNT").unwrap();
2942 assert_eq!(rows[0]["count"], json!(0));
2943 let (s, _) = query(&db, "FROM nonexistent SUM n_tx").unwrap();
2944 assert_eq!(s[0]["sum_n_tx"], Value::Null, "sum of nothing is null, not 0");
2945 }
2946
2947 #[test]
2948 fn bare_aggregate_carries_no_group_key() {
2949 let (_tmp, db) = setup();
2950 let (rows, _) = query(&db, "FROM blocks COUNT").unwrap();
2951 if let Value::Object(m) = &rows[0] {
2952 let mut keys: Vec<&String> = m.keys().collect();
2953 keys.sort();
2954 assert_eq!(keys, vec!["count", "value"]);
2955 } else {
2956 panic!("expected an object");
2957 }
2958 }
2959
2960 #[test]
2967 fn a_field_named_like_a_keyword_is_still_addressable() {
2968 let dir = tempdir().unwrap();
2969 let db = Db::open(dir.path(), None).unwrap();
2970 db.put("t", "1", serde_json::json!({
2971 "count": 5, "min": 1, "max": 9, "sum": 3, "avg": 2,
2972 "value": "keep", "limit": 7, "offset": 8, "group": "g1", "search": "s",
2973 }), vec![], None, None).unwrap();
2974 db.put("t", "2", serde_json::json!({
2975 "count": 1, "min": 0, "max": 2, "sum": 0, "avg": 0,
2976 "value": "drop", "limit": 0, "offset": 0, "group": "g2", "search": "t",
2977 }), vec![], None, None).unwrap();
2978
2979 for (nql, want) in [
2980 ("FROM t WHERE count > 3", "1"),
2981 ("FROM t WHERE min = 1", "1"),
2982 ("FROM t WHERE max >= 9", "1"),
2983 ("FROM t WHERE sum = 3", "1"),
2984 ("FROM t WHERE avg = 2", "1"),
2985 (r#"FROM t WHERE value = "keep""#, "1"),
2986 ("FROM t WHERE limit = 7", "1"),
2987 ("FROM t WHERE offset = 8", "1"),
2988 (r#"FROM t WHERE group = "g1""#, "1"),
2989 ] {
2990 let (rows, _) = query(&db, nql).unwrap();
2991 assert_eq!(rows.len(), 1, "`{}` matched {} rows", nql, rows.len());
2992 assert_eq!(rows[0]["_id"], want, "`{}`", nql);
2993 }
2994
2995 let (ord, _) = query(&db, "FROM t ORDER BY count DESC").unwrap();
2997 assert_eq!(ord[0]["_id"], "1");
2998 let (grp, _) = query(&db, "FROM t GROUP BY group COUNT").unwrap();
2999 assert_eq!(grp.len(), 2);
3000 let keys: Vec<&str> = grp.iter().filter_map(|r| r["group"].as_str()).collect();
3001 assert!(keys.contains(&"g1") && keys.contains(&"g2"), "{:?}", grp);
3002 }
3003
3004 #[test]
3007 fn keyword_matching_stays_case_insensitive() {
3008 let dir = tempdir().unwrap();
3009 let db = Db::open(dir.path(), None).unwrap();
3010 db.put("t", "1", serde_json::json!({"Count": 5, "n": 1}),
3011 vec![], None, None).unwrap();
3012 let (rows, _) = query(&db, "from t where Count = 5 order by n").unwrap();
3014 assert_eq!(rows.len(), 1);
3015 let (miss, _) = query(&db, "FROM t WHERE count = 5").unwrap();
3017 assert!(miss.is_empty(), "`count` and `Count` are distinct field names");
3018 }
3019
3020 #[test]
3021 fn two_aggregates_is_an_error() {
3022 let (_tmp, db) = setup();
3023 assert!(query(&db, "FROM blocks COUNT SUM n_tx").is_err());
3024 assert!(query(&db, "FROM blocks GROUP BY height COUNT SUM n_tx").is_err());
3025 }
3026
3027 #[test]
3028 fn bare_aggregate_with_having() {
3029 let (_tmp, db) = setup();
3030 let (keep, _) = query(&db, "FROM blocks COUNT HAVING count > 3").unwrap();
3031 assert_eq!(keep.len(), 1);
3032 let (drop, _) = query(&db, "FROM blocks COUNT HAVING count > 99").unwrap();
3033 assert!(drop.is_empty());
3034 }
3035
3036 fn setup_items() -> (tempfile::TempDir, Db) {
3041 let dir = tempdir().unwrap();
3042 let db = Db::open(dir.path(), None).unwrap();
3043 for i in 0..6 {
3044 db.put("items", &i.to_string(),
3045 serde_json::json!({"cat": if i < 3 {"x"} else {"y"}, "price": i * 5}),
3046 vec![], None, None).unwrap();
3047 }
3048 (dir, db)
3049 }
3050
3051 fn group(rows: &[Value], field: &str, key: &str) -> Value {
3052 rows.iter()
3053 .find(|r| r[field] == Value::String(key.to_string()))
3054 .unwrap_or_else(|| panic!("no group {:?} in {:?}", key, rows))
3055 .clone()
3056 }
3057
3058 #[test]
3064 fn group_by_aggregates_the_target_field_not_the_group_field() {
3065 let (_tmp, db) = setup_items();
3066
3067 let (mins, _) = query(&db, "FROM items GROUP BY cat MIN price").unwrap();
3068 assert_eq!(group(&mins, "cat", "x")["min_price"], json!(0));
3069 assert_eq!(group(&mins, "cat", "y")["min_price"], json!(15));
3070
3071 let (maxs, _) = query(&db, "FROM items GROUP BY cat MAX price").unwrap();
3072 assert_eq!(group(&maxs, "cat", "y")["max_price"], json!(25));
3073 assert_eq!(group(&maxs, "cat", "x")["max_price"], json!(10));
3074
3075 let (sums, _) = query(&db, "FROM items GROUP BY cat SUM price").unwrap();
3076 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();
3080 assert_eq!(group(&avgs, "cat", "x")["avg_price"], json!(5.0));
3081 assert_eq!(group(&avgs, "cat", "y")["avg_price"], json!(20.0));
3082 }
3083
3084 #[test]
3096 fn aggregates_see_node_metadata_not_only_the_payload() {
3097 let (_tmp, db) = setup_items();
3098 let (all, _) = query(&db, "FROM items").unwrap();
3099 let want = all.iter().filter_map(|r| r.get("_seq")?.as_i64()).max().unwrap();
3100
3101 let (rows, _) = query(&db, "FROM items MAX _seq").unwrap();
3102 assert_eq!(rows[0]["max__seq"], json!(want),
3103 "MAX _seq must equal the highest sequence in the result");
3104 assert_ne!(rows[0]["max__seq"], Value::Null, "a null here is a silent wrong answer");
3105
3106 let (rows, _) = query(&db, "FROM items MIN _seq").unwrap();
3107 assert_eq!(rows[0]["min__seq"], json!(
3108 all.iter().filter_map(|r| r.get("_seq")?.as_i64()).min().unwrap()));
3109
3110 let (rows, _) = query(&db, "FROM items GROUP BY _seq COUNT").unwrap();
3112 assert_eq!(rows.len(), all.len(), "one group per distinct sequence");
3113 assert!(rows.iter().all(|r| r["_seq"] != Value::Null),
3114 "the group key must be the sequence, not null");
3115 }
3116
3117 #[test]
3121 fn group_by_emits_python_parity_keys_and_the_value_alias() {
3122 let (_tmp, db) = setup_items();
3123 let (rows, _) = query(&db, "FROM items GROUP BY cat SUM price").unwrap();
3124 let x = group(&rows, "cat", "x");
3125 assert_eq!(x["sum_price"], json!(15), "python-parity key");
3126 assert_eq!(x["value"], json!(15), "back-compat alias must agree");
3127 assert_eq!(x["count"], json!(3), "count is the group size");
3128 }
3129
3130 #[test]
3134 fn count_is_group_size_while_aggregate_skips_non_numeric() {
3135 let dir = tempdir().unwrap();
3136 let db = Db::open(dir.path(), None).unwrap();
3137 db.put("t", "1", serde_json::json!({"g": "a", "n": 10}), vec![], None, None).unwrap();
3138 db.put("t", "2", serde_json::json!({"g": "a", "n": 20}), vec![], None, None).unwrap();
3139 db.put("t", "3", serde_json::json!({"g": "a", "n": "N/A"}), vec![], None, None).unwrap();
3140
3141 let (rows, _) = query(&db, "FROM t GROUP BY g AVG n").unwrap();
3142 let a = group(&rows, "g", "a");
3143 assert_eq!(a["count"], json!(3), "every row counts toward the group");
3144 assert_eq!(a["avg_n"], json!(15.0), "only the two numeric rows average");
3145 }
3146
3147 #[test]
3149 fn empty_aggregate_input_is_null() {
3150 let dir = tempdir().unwrap();
3151 let db = Db::open(dir.path(), None).unwrap();
3152 db.put("t", "1", serde_json::json!({"g": "a", "n": "x"}), vec![], None, None).unwrap();
3153 let (rows, _) = query(&db, "FROM t GROUP BY g MIN n").unwrap();
3154 assert_eq!(rows[0]["min_n"], Value::Null);
3155 assert_eq!(rows[0]["count"], json!(1));
3156 }
3157
3158 #[test]
3161 fn bare_group_by_without_an_aggregate_counts() {
3162 let (_tmp, db) = setup_items();
3163 let (rows, _) = query(&db, "FROM items GROUP BY cat").unwrap();
3164 assert_eq!(rows.len(), 2);
3165 assert_eq!(group(&rows, "cat", "x")["count"], json!(3));
3166 assert_eq!(group(&rows, "cat", "y")["count"], json!(3));
3167 }
3168
3169 #[test]
3171 fn aggregate_without_a_target_field_is_an_error() {
3172 let (_tmp, db) = setup_items();
3173 for bad in [
3174 "FROM items GROUP BY cat SUM",
3175 "FROM items GROUP BY cat AVG",
3176 "FROM items GROUP BY cat MIN",
3177 ] {
3178 assert!(query(&db, bad).is_err(), "`{}` must be rejected", bad);
3179 }
3180 }
3181
3182 #[test]
3185 fn group_order_is_stable_across_runs() {
3186 let (_tmp, db) = setup_items();
3187 let first = query(&db, "FROM items GROUP BY cat SUM price").unwrap().0;
3188 for _ in 0..8 {
3189 let again = query(&db, "FROM items GROUP BY cat SUM price").unwrap().0;
3190 assert_eq!(first, again);
3191 }
3192 }
3193
3194 #[test]
3196 fn group_by_after_an_in_predicate() {
3197 let (_tmp, db) = setup_items();
3198 let (rows, _) = query(
3199 &db, "FROM items WHERE price IN (0, 5, 25) GROUP BY cat SUM price").unwrap();
3200 assert_eq!(group(&rows, "cat", "x")["sum_price"], json!(5));
3201 assert_eq!(group(&rows, "cat", "y")["sum_price"], json!(25));
3202 }
3203
3204 #[test]
3205 fn search() {
3206 let (_tmp, db) = setup();
3207 let (rows, _) = query(&db, r#"FROM blocks SEARCH "0003""#).unwrap();
3208 assert_eq!(rows.len(), 1);
3209 }
3210
3211 #[test]
3212 fn as_of() {
3213 let dir = tempdir().unwrap();
3214 let db = Db::open(dir.path(), None).unwrap();
3215 let v1 = db.put("docs", "x", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
3216 db.put("docs", "x", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
3217 let (rows, _) = query(&db, &format!("FROM docs AS OF {}", v1.seq)).unwrap();
3218 assert_eq!(rows[0]["v"], 1);
3219 }
3220
3221 #[test]
3222 fn valid_as_of() {
3223 let dir = tempdir().unwrap();
3224 let db = Db::open(dir.path(), None).unwrap();
3225 db.put("events", "e1", serde_json::json!({"type": "a"}), vec![],
3226 Some("2025-01-01".to_string()), Some("2025-06-01".to_string())).unwrap();
3227 db.put("events", "e2", serde_json::json!({"type": "b"}), vec![],
3228 Some("2026-01-01".to_string()), None).unwrap();
3229 let (rows, _) = query(&db, r#"FROM events VALID AS OF "2025-03-01""#).unwrap();
3230 assert_eq!(rows.len(), 1);
3231 assert_eq!(rows[0]["type"], "a");
3232 }
3233
3234 #[test]
3237 fn escaped_quote_matches_a_value_containing_a_quote() {
3238 let dir = tempdir().unwrap();
3239 let db = Db::open(dir.path(), None).unwrap();
3240 db.put("m", "q", serde_json::json!({ "name": "say \"hi\"" }), vec![], None, None)
3241 .unwrap();
3242 db.put("m", "p", serde_json::json!({ "name": "plain" }), vec![], None, None).unwrap();
3243
3244 let (rows, count) = query(&db, r#"FROM m WHERE name = "say \"hi\"""#).unwrap();
3246 assert_eq!(count, 1, "the escaped-quote literal matches exactly one row");
3247 assert_eq!(rows[0]["_id"], "q");
3248 }
3249
3250 #[test]
3251 fn raw_backslash_still_matches_literally() {
3252 let dir = tempdir().unwrap();
3256 let db = Db::open(dir.path(), None).unwrap();
3257 db.put("m", "b", serde_json::json!({ "p": "back\\slash" }), vec![], None, None).unwrap();
3258
3259 let (rows, count) = query(&db, r#"FROM m WHERE p = "back\slash""#).unwrap();
3260 assert_eq!(count, 1, "a raw backslash literal matches as before");
3261 assert_eq!(rows[0]["_id"], "b");
3262 }
3263
3264 #[test]
3265 fn a_quote_can_no_longer_inject_trailing_clauses() {
3266 let dir = tempdir().unwrap();
3271 let db = Db::open(dir.path(), None).unwrap();
3272 db.put("m", "x", serde_json::json!({ "v": "a\"b" }), vec![], None, None).unwrap();
3273 let (rows, count) = query(&db, r#"FROM m WHERE v = "a\"b""#).unwrap();
3274 assert_eq!(count, 1);
3275 assert_eq!(rows[0]["_id"], "x");
3276 }
3277}
3278
3279#[cfg(test)]
3280mod tests_traverse {
3281 use super::*;
3282 use tempfile::tempdir;
3283 use crate::db::Db;
3284
3285 #[test]
3286 fn traverse_one_hop() {
3287 let db = Db::in_memory();
3288 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
3289 db.put("driver", "d2", serde_json::json!({"name": "Carol"}), vec![], None, None).unwrap();
3290 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
3291 db.put("trip", "t2", serde_json::json!({"status": "ok"}), vec![], None, None).unwrap();
3292
3293 db.link("driver:d1", "handles", "trip:t1").unwrap();
3294 db.link("driver:d1", "handles", "trip:t2").unwrap();
3295
3296 let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3297 assert_eq!(count, 2);
3298 let ids: std::collections::HashSet<&str> = rows.iter()
3299 .filter_map(|r| r["_id"].as_str())
3300 .collect();
3301 assert!(ids.contains("t1") && ids.contains("t2"));
3302 }
3303
3304 #[test]
3305 fn traverse_returns_empty_when_no_links() {
3306 let db = Db::in_memory();
3307 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
3308 let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3309 assert_eq!(count, 0);
3310 assert!(rows.is_empty());
3311 }
3312
3313 #[test]
3314 fn traverse_multi_source() {
3315 let db = Db::in_memory();
3317 db.put("driver", "d1", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
3318 db.put("driver", "d2", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
3319 db.put("trip", "t1", serde_json::json!({"n": 1}), vec![], None, None).unwrap();
3320 db.put("trip", "t2", serde_json::json!({"n": 2}), vec![], None, None).unwrap();
3321 db.put("trip", "t3", serde_json::json!({"n": 3}), vec![], None, None).unwrap();
3322
3323 db.link("driver:d1", "handles", "trip:t1").unwrap();
3324 db.link("driver:d1", "handles", "trip:t2").unwrap();
3325 db.link("driver:d2", "handles", "trip:t3").unwrap();
3326
3327 let (_rows, count) = query(&db, r#"FROM driver WHERE status = "active" TRAVERSE handles"#).unwrap();
3328 assert_eq!(count, 3);
3329 }
3330
3331 #[test]
3332 fn traverse_nql_keyword_case_insensitive() {
3333 let db = Db::in_memory();
3335 db.put("driver", "d1", serde_json::json!({}), vec![], None, None).unwrap();
3336 db.put("trip", "t1", serde_json::json!({}), vec![], None, None).unwrap();
3337 db.link("driver:d1", "handles", "trip:t1").unwrap();
3338 let (r1, c1) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3340 assert_eq!(c1, 1);
3341 let (r2, c2) = query(&db, r#"FROM driver WHERE _id = "d1" traverse handles"#).unwrap();
3343 assert_eq!(c2, 1);
3344 assert_eq!(r1[0]["_id"], r2[0]["_id"]);
3345 }
3346
3347 #[test]
3348 fn traverse_durable() {
3349 let dir = tempdir().unwrap();
3350 {
3351 let db = Db::open(dir.path(), None).unwrap();
3352 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
3353 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
3354 db.link("driver:d1", "handles", "trip:t1").unwrap();
3355 }
3356 let db2 = Db::open(dir.path(), None).unwrap();
3357 db2.startup_ready.store(true, std::sync::atomic::Ordering::SeqCst);
3358 let (rows, count) = query(&db2, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
3359 assert_eq!(count, 1);
3360 assert_eq!(rows[0]["_id"], "t1");
3361 }
3362}