1use std::collections::HashMap;
45use anyhow::{bail, Result};
46use serde_json::{json, Value};
47
48use crate::db::Db;
49use crate::index::OrderedValue;
50use crate::store::Node;
51
52#[derive(Debug, Clone, PartialEq)]
55enum Tok {
56 Kw(String, String),
65 Ident(String), Str(String), Num(f64), Op(String), Punct(char), Eof,
71}
72
73struct Lexer<'a> {
74 src: &'a str,
75 pos: usize,
76}
77
78impl<'a> Lexer<'a> {
79 fn new(src: &'a str) -> Self { Self { src, pos: 0 } }
80
81 fn peek_char(&self) -> Option<char> { self.src[self.pos..].chars().next() }
82
83 fn skip_ws(&mut self) {
84 while let Some(c) = self.peek_char() {
85 if c.is_whitespace() { self.pos += c.len_utf8(); } else { break; }
86 }
87 }
88
89 fn next_tok(&mut self) -> Tok {
90 self.skip_ws();
91 if self.pos >= self.src.len() { return Tok::Eof; }
92
93 let c = self.peek_char().unwrap();
94
95 if c == '"' {
107 self.pos += 1;
108 let mut s = String::new();
109 while let Some(ch) = self.peek_char() {
110 if ch == '"' {
111 break;
112 }
113 if ch == '\\' {
114 let next = self.src[self.pos + 1..].chars().next();
118 if next == Some('"') {
119 s.push('"');
120 self.pos += 1 + 1; continue;
122 }
123 }
124 s.push(ch);
125 self.pos += ch.len_utf8();
126 }
127 if self.peek_char() == Some('"') {
128 self.pos += 1;
129 }
130 return Tok::Str(s);
131 }
132
133 if self.pos + 1 < self.src.len() {
135 let two = &self.src[self.pos..self.pos+2];
136 if matches!(two, "!=" | ">=" | "<=") {
137 self.pos += 2;
138 return Tok::Op(two.to_string());
139 }
140 }
141
142 if matches!(c, '=' | '>' | '<') {
144 self.pos += 1;
145 return Tok::Op(c.to_string());
146 }
147
148 if matches!(c, '(' | ')' | ',') {
153 self.pos += 1;
154 return Tok::Punct(c);
155 }
156
157 if c.is_ascii_digit() || (c == '-' && self.src[self.pos+1..].starts_with(|d: char| d.is_ascii_digit())) {
159 let start = self.pos;
160 if c == '-' { self.pos += 1; }
161 while let Some(d) = self.peek_char() {
162 if d.is_ascii_digit() || d == '.' { self.pos += 1; } else { break; }
163 }
164 let n: f64 = self.src[start..self.pos].parse().unwrap_or(0.0);
165 return Tok::Num(n);
166 }
167
168 if c.is_alphabetic() || c == '_' {
170 let start = self.pos;
171 while let Some(ch) = self.peek_char() {
172 if ch.is_alphanumeric() || ch == '_' || ch == '.' || ch == ':' {
173 self.pos += ch.len_utf8();
174 } else { break; }
175 }
176 let word = &self.src[start..self.pos];
177 let upper = word.to_uppercase();
178 let keywords = ["FROM","AS","OF","VALID","WHERE","AND","OR","ORDER","BY",
179 "ASC","DESC","LIMIT","OFFSET","GROUP","HAVING",
180 "COUNT","SUM","AVG","MIN","MAX",
181 "TRACE","TRAVERSE","REVERSE","SEARCH","NOT","NULL","TRUE","FALSE",
182 "IN","BETWEEN","LIKE","ILIKE","IS"];
183 if keywords.contains(&upper.as_str()) {
184 return Tok::Kw(upper, word.to_string());
185 }
186 return Tok::Ident(word.to_string());
187 }
188
189 self.pos += c.len_utf8();
191 self.next_tok()
192 }
193
194 fn tokenize(&mut self) -> Vec<Tok> {
195 let mut toks = vec![];
196 loop {
197 let t = self.next_tok();
198 if t == Tok::Eof { break; }
199 toks.push(t);
200 }
201 toks
202 }
203}
204
205#[derive(Debug, Clone)]
214pub enum Pred {
215 Cmp { field: String, op: String, value: Value },
217 In { field: String, values: Vec<Value>, negated: bool },
219 Between { field: String, low: Value, high: Value, negated: bool },
221 Like { field: String, pattern: String, negated: bool, ci: bool },
224 IsNull { field: String, negated: bool },
226 And(Vec<Pred>),
227 Or(Vec<Pred>),
228 Not(Box<Pred>),
229}
230
231#[derive(Debug, Clone, PartialEq)]
232pub enum GroupAgg { Count, Sum, Avg, Min, Max }
233
234impl GroupAgg {
235 fn name(&self) -> &'static str {
236 match self {
237 GroupAgg::Count => "count", GroupAgg::Sum => "sum",
238 GroupAgg::Avg => "avg", GroupAgg::Min => "min",
239 GroupAgg::Max => "max",
240 }
241 }
242}
243
244#[derive(Debug, Clone, PartialEq)]
249pub struct OrderKey {
250 pub field: String,
251 pub desc: bool,
252}
253
254#[derive(Debug, Clone)]
261pub struct Aggregate {
262 pub group_field: Option<String>,
263 pub agg: GroupAgg,
264 pub agg_field: Option<String>,
266}
267
268#[derive(Debug, Clone)]
269pub struct Query {
270 pub coll: String,
271 pub as_of: Option<u64>,
272 pub valid_as_of: Option<String>,
273 pub where_: Option<Pred>,
274 pub search: Option<String>,
275 pub order_by: Vec<OrderKey>,
276 pub limit: Option<usize>,
277 pub offset: Option<usize>,
278 pub aggregate: Option<Aggregate>,
279 pub having: Option<Pred>,
283 pub trace: Option<String>, pub trace_rev: bool,
285 pub traverse: Option<String>, }
287
288struct Parser { toks: Vec<Tok>, pos: usize }
291
292impl Parser {
293 fn new(toks: Vec<Tok>) -> Self { Self { toks, pos: 0 } }
294
295 fn peek(&self) -> &Tok { self.toks.get(self.pos).unwrap_or(&Tok::Eof) }
296 fn advance(&mut self) -> Tok { let t = self.peek().clone(); self.pos += 1; t }
297
298 fn expect_kw(&mut self, kw: &str) -> Result<()> {
299 match self.advance() {
300 Tok::Kw(k, _) if k == kw => Ok(()),
301 other => bail!("expected keyword {}, got {:?}", kw, other),
302 }
303 }
304
305 fn parse_value(&mut self) -> Result<Value> {
312 Ok(match self.advance() {
313 Tok::Str(s) => Value::String(s),
314 Tok::Num(n) => json!(n),
315 Tok::Kw(k, _) if k == "NULL" => Value::Null,
316 Tok::Kw(k, _) if k == "TRUE" => Value::Bool(true),
317 Tok::Kw(k, _) if k == "FALSE" => Value::Bool(false),
318 Tok::Ident(s) => Value::String(s),
319 other => bail!("expected a value (string, number, TRUE, FALSE or NULL), got {:?}", other),
320 })
321 }
322
323 fn peek_kw(&self, kw: &str) -> bool {
324 matches!(self.peek(), Tok::Kw(k, _) if k == kw)
325 }
326
327 fn eat_kw(&mut self, kw: &str) -> bool {
328 if self.peek_kw(kw) { self.advance(); true } else { false }
329 }
330
331 fn expect_punct(&mut self, c: char) -> Result<()> {
332 match self.advance() {
333 Tok::Punct(p) if p == c => Ok(()),
334 other => bail!("expected '{}', got {:?}", c, other),
335 }
336 }
337
338 fn parse_agg_kw(&mut self) -> Result<GroupAgg> {
339 Ok(match self.advance() {
340 Tok::Kw(a, _) if a == "COUNT" => GroupAgg::Count,
341 Tok::Kw(a, _) if a == "SUM" => GroupAgg::Sum,
342 Tok::Kw(a, _) if a == "AVG" => GroupAgg::Avg,
343 Tok::Kw(a, _) if a == "MIN" => GroupAgg::Min,
344 Tok::Kw(a, _) if a == "MAX" => GroupAgg::Max,
345 other => bail!("expected an aggregate (COUNT/SUM/AVG/MIN/MAX), got {:?}", other),
346 })
347 }
348
349 fn parse_field(&mut self, ctx: &str) -> Result<String> {
350 match self.advance() {
351 Tok::Ident(s) | Tok::Kw(_, s) => Ok(s),
352 other => bail!("{}: expected field name, got {:?}", ctx, other),
353 }
354 }
355
356 fn parse_pred(&mut self) -> Result<Pred> { self.parse_or() }
359
360 fn parse_or(&mut self) -> Result<Pred> {
361 let mut terms = vec![self.parse_and()?];
362 while self.eat_kw("OR") {
363 terms.push(self.parse_and()?);
364 }
365 Ok(if terms.len() == 1 { terms.pop().unwrap() } else { Pred::Or(terms) })
366 }
367
368 fn parse_and(&mut self) -> Result<Pred> {
369 let mut terms = vec![self.parse_not()?];
370 while self.peek_kw("AND") {
371 self.advance();
374 terms.push(self.parse_not()?);
375 }
376 Ok(if terms.len() == 1 { terms.pop().unwrap() } else { Pred::And(terms) })
377 }
378
379 fn parse_not(&mut self) -> Result<Pred> {
380 if self.eat_kw("NOT") {
381 return Ok(Pred::Not(Box::new(self.parse_not()?)));
382 }
383 self.parse_primary()
384 }
385
386 fn parse_primary(&mut self) -> Result<Pred> {
387 if matches!(self.peek(), Tok::Punct('(')) {
388 self.advance();
389 let inner = self.parse_pred()?;
390 self.expect_punct(')')?;
391 return Ok(inner);
392 }
393 self.parse_comparison()
394 }
395
396 fn parse_comparison(&mut self) -> Result<Pred> {
397 let field = self.parse_field("WHERE")?;
398
399 if self.eat_kw("IS") {
401 let negated = self.eat_kw("NOT");
402 if !self.eat_kw("NULL") {
403 bail!("WHERE: expected NULL after IS{}", if negated { " NOT" } else { "" });
404 }
405 return Ok(Pred::IsNull { field, negated });
406 }
407
408 let negated = self.eat_kw("NOT");
410
411 if self.eat_kw("IN") {
412 self.expect_punct('(')?;
413 let mut values = vec![];
414 loop {
415 values.push(self.parse_value()?);
416 if matches!(self.peek(), Tok::Punct(',')) { self.advance(); continue; }
417 break;
418 }
419 self.expect_punct(')')?;
420 if values.is_empty() {
421 bail!("WHERE: IN () needs at least one value");
422 }
423 return Ok(Pred::In { field, values, negated });
424 }
425
426 if self.eat_kw("BETWEEN") {
427 let low = self.parse_value()?;
428 if !self.eat_kw("AND") {
429 bail!("WHERE: BETWEEN expects AND between its bounds");
430 }
431 let high = self.parse_value()?;
432 return Ok(Pred::Between { field, low, high, negated });
433 }
434
435 let ci = self.peek_kw("ILIKE");
436 if ci || self.peek_kw("LIKE") {
437 self.advance();
438 let pattern = match self.advance() {
439 Tok::Str(s) => s,
440 Tok::Ident(s) => s,
441 other => bail!("WHERE: LIKE expects a pattern string, got {:?}", other),
442 };
443 return Ok(Pred::Like { field, pattern, negated, ci });
444 }
445
446 if negated {
447 bail!("WHERE: NOT must be followed by IN, BETWEEN, LIKE or ILIKE \
448 (use `NOT (field = value)` or `field != value` to negate a comparison)");
449 }
450
451 let op = match self.advance() {
452 Tok::Op(s) => s,
453 other => bail!("WHERE: expected operator, got {:?}", other),
454 };
455 let value = self.parse_value()?;
456 Ok(Pred::Cmp { field, op, value })
457 }
458
459 fn parse(&mut self) -> Result<Query> {
460 self.expect_kw("FROM")?;
461 let coll = match self.advance() {
462 Tok::Ident(s) | Tok::Kw(_, s) => s,
463 other => bail!("expected collection name, got {:?}", other),
464 };
465
466 let mut q = Query {
467 coll, as_of: None, valid_as_of: None,
468 where_: None, search: None,
469 order_by: vec![],
470 limit: None, offset: None,
471 aggregate: None, having: None,
472 trace: None, trace_rev: false,
473 traverse: None,
474 };
475
476 loop {
477 match self.peek() {
478 Tok::Eof => break,
479
480 Tok::Kw(k, _) if k == "AS" => {
481 self.advance();
482 self.expect_kw("OF")?;
483 match self.advance() {
484 Tok::Num(n) => q.as_of = Some(n as u64),
485 other => bail!("AS OF expects sequence number, got {:?}", other),
486 }
487 }
488
489 Tok::Kw(k, _) if k == "VALID" => {
490 self.advance();
491 self.expect_kw("AS")?;
492 self.expect_kw("OF")?;
493 match self.advance() {
494 Tok::Str(s) => q.valid_as_of = Some(s),
495 other => bail!("VALID AS OF expects date string, got {:?}", other),
496 }
497 }
498
499 Tok::Kw(k, _) if k == "WHERE" => {
500 self.advance();
501 let pred = self.parse_pred()?;
502 q.where_ = Some(match q.where_.take() {
505 None => pred,
506 Some(prev) => Pred::And(vec![prev, pred]),
507 });
508 }
509
510 Tok::Kw(k, _) if k == "SEARCH" => {
511 self.advance();
512 match self.advance() {
513 Tok::Str(s) => q.search = Some(s),
514 other => bail!("SEARCH expects quoted string, got {:?}", other),
515 }
516 }
517
518 Tok::Kw(k, _) if k == "ORDER" => {
519 self.advance();
520 self.expect_kw("BY")?;
521 loop {
524 let field = self.parse_field("ORDER BY")?;
525 let desc = if self.eat_kw("DESC") {
529 true
530 } else {
531 self.eat_kw("ASC");
532 false
533 };
534 q.order_by.push(OrderKey { field, desc });
535 if matches!(self.peek(), Tok::Punct(',')) { self.advance(); continue; }
536 break;
537 }
538 }
539
540 Tok::Kw(k, _) if k == "LIMIT" => {
541 self.advance();
542 match self.advance() {
543 Tok::Num(n) if n >= 0.0 => q.limit = Some(n as usize),
544 other => bail!("LIMIT expects a non-negative number, got {:?}", other),
545 }
546 }
547
548 Tok::Kw(k, _) if k == "OFFSET" => {
549 self.advance();
550 match self.advance() {
551 Tok::Num(n) if n >= 0.0 => q.offset = Some(n as usize),
552 other => bail!("OFFSET expects a non-negative number, got {:?}", other),
553 }
554 }
555
556 Tok::Kw(k, _) if k == "HAVING" => {
557 self.advance();
558 let pred = self.parse_pred()?;
559 q.having = Some(match q.having.take() {
560 None => pred,
561 Some(prev) => Pred::And(vec![prev, pred]),
562 });
563 }
564
565 Tok::Kw(k, _) if matches!(k.as_str(), "COUNT" | "SUM" | "AVG" | "MIN" | "MAX") => {
568 let agg = self.parse_agg_kw()?;
569 let agg_field = match agg {
570 GroupAgg::Count => None,
571 _ => Some(self.parse_field("aggregate")?),
572 };
573 if q.aggregate.is_some() {
574 bail!("only one aggregate per query");
575 }
576 q.aggregate = Some(Aggregate { group_field: None, agg, agg_field });
577 }
578
579 Tok::Kw(k, _) if k == "GROUP" => {
580 self.advance();
581 self.expect_kw("BY")?;
582 let field = match self.advance() {
583 Tok::Ident(s) | Tok::Kw(_, s) => s,
584 other => bail!("GROUP BY: expected field, got {:?}", other),
585 };
586 let agg = if matches!(self.peek(),
591 Tok::Kw(a, _) if matches!(a.as_str(), "COUNT"|"SUM"|"AVG"|"MIN"|"MAX"))
592 {
593 self.parse_agg_kw()?
594 } else {
595 GroupAgg::Count
596 };
597 let agg_field = match agg {
604 GroupAgg::Count => None,
605 _ => Some(self.parse_field("GROUP BY aggregate")?),
606 };
607 if q.aggregate.is_some() {
608 bail!("only one aggregate per query");
609 }
610 q.aggregate = Some(Aggregate {
611 group_field: Some(field), agg, agg_field,
612 });
613 }
614
615 Tok::Kw(k, _) if k == "TRACE" => {
616 self.advance();
617 let edge = match self.advance() {
618 Tok::Ident(s) | Tok::Kw(_, s) => s,
619 other => bail!("TRACE: expected edge type, got {:?}", other),
620 };
621 q.trace = Some(edge);
622 if let Tok::Kw(k, _) = self.peek() {
623 if k == "REVERSE" { self.advance(); q.trace_rev = true; }
624 }
625 }
626
627 Tok::Kw(k, _) if k == "TRAVERSE" => {
628 self.advance();
629 let rel = match self.advance() {
630 Tok::Ident(s) | Tok::Kw(_, s) => s,
631 other => bail!("TRAVERSE: expected relation name, got {:?}", other),
632 };
633 q.traverse = Some(rel);
634 }
635
636 other => bail!(
639 "unexpected {:?} in query. Expected one of: AS OF, VALID AS OF, \
640 WHERE, SEARCH, ORDER BY, LIMIT, OFFSET, GROUP BY, HAVING, \
641 COUNT, SUM, AVG, MIN, MAX, TRACE, TRAVERSE",
642 other
643 ),
644 }
645 }
646
647 Ok(q)
648 }
649}
650
651fn field_value(node: &Node, field: &str) -> Value {
656 match field {
657 "_id" => Value::String(node.id.clone()),
658 "_coll" => Value::String(node.coll.clone()),
659 "_hash" => Value::String(node.hash.clone()),
660 "_seq" => json!(node.seq),
661 _ => node.data.get(field).cloned().unwrap_or(Value::Null),
662 }
663}
664
665fn cmp_op(a: &Value, op: &str, b: &Value) -> bool {
666 if matches!(op, "<" | "<=" | ">" | ">=") && a.is_null() {
687 return false;
688 }
689 let a = OrderedValue::from(a);
690 let b = OrderedValue::from(b);
691 match op {
692 "=" => a == b,
693 "!=" => a != b,
694 ">" => a > b,
695 "<" => a < b,
696 ">=" => a >= b,
697 "<=" => a <= b,
698 _ => false,
699 }
700}
701
702fn as_text(v: &Value) -> String {
706 match v {
707 Value::String(s) => s.clone(),
708 Value::Null => String::new(),
709 other => other.to_string(),
710 }
711}
712
713fn like_match(value: &str, pattern: &str, ci: bool) -> bool {
718 let (v, p): (Vec<char>, Vec<char>) = if ci {
719 (value.to_lowercase().chars().collect(), pattern.to_lowercase().chars().collect())
720 } else {
721 (value.chars().collect(), pattern.chars().collect())
722 };
723
724 let mut vi = 0usize;
725 let mut pi = 0usize;
726 let mut star: Option<(usize, usize)> = None;
728
729 while vi < v.len() {
730 if pi < p.len() && (p[pi] == '_' || p[pi] == v[vi]) {
731 vi += 1;
732 pi += 1;
733 } else if pi < p.len() && p[pi] == '%' {
734 star = Some((pi, vi));
735 pi += 1;
736 } else if let Some((sp, sv)) = star {
737 pi = sp + 1;
739 vi = sv + 1;
740 star = Some((sp, vi));
741 } else {
742 return false;
743 }
744 }
745 while pi < p.len() && p[pi] == '%' { pi += 1; }
747 pi == p.len()
748}
749
750fn eval_pred_with(get: &dyn Fn(&str) -> Value, pred: &Pred) -> bool {
757 match pred {
758 Pred::Cmp { field, op, value } => cmp_op(&get(field), op, value),
759
760 Pred::In { field, values, negated } => {
761 let fv = get(field);
762 let hit = values.iter().any(|v| cmp_op(&fv, "=", v));
763 hit != *negated
764 }
765
766 Pred::Between { field, low, high, negated } => {
767 let fv = get(field);
768 let hit = cmp_op(&fv, ">=", low) && cmp_op(&fv, "<=", high);
770 hit != *negated
771 }
772
773 Pred::Like { field, pattern, negated, ci } => {
774 let fv = get(field);
775 if fv.is_null() { return false; }
779 let hit = like_match(&as_text(&fv), pattern, *ci);
780 hit != *negated
781 }
782
783 Pred::IsNull { field, negated } => {
784 get(field).is_null() != *negated
788 }
789
790 Pred::And(terms) => terms.iter().all(|t| eval_pred_with(get, t)),
791 Pred::Or(terms) => terms.iter().any(|t| eval_pred_with(get, t)),
792 Pred::Not(inner) => !eval_pred_with(get, inner),
793 }
794}
795
796fn eval_pred(node: &Node, pred: &Pred) -> bool {
797 eval_pred_with(&|f| field_value(node, f), pred)
798}
799
800fn eval_pred_json(obj: &Value, pred: &Pred) -> bool {
802 eval_pred_with(&|f| obj.get(f).cloned().unwrap_or(Value::Null), pred)
803}
804
805fn sort_by_keys<T>(rows: &mut [T], keys: &[OrderKey], get: impl Fn(&T, &str) -> Value) {
808 rows.sort_by(|a, b| {
809 for k in keys {
810 let av = OrderedValue::from(&get(a, &k.field));
811 let bv = OrderedValue::from(&get(b, &k.field));
812 let ord = if k.desc { bv.cmp(&av) } else { av.cmp(&bv) };
813 if ord != std::cmp::Ordering::Equal {
814 return ord;
815 }
816 }
817 std::cmp::Ordering::Equal
818 });
819}
820
821fn paginate<T>(rows: Vec<T>, offset: Option<usize>, limit: Option<usize>) -> Vec<T> {
826 let mut it = rows;
827 if let Some(off) = offset {
828 if off >= it.len() {
829 return vec![];
830 }
831 it.drain(..off);
832 }
833 if let Some(n) = limit {
834 it.truncate(n);
835 }
836 it
837}
838
839fn aggregate_rows(rows: &[Node], spec: &Aggregate) -> Vec<Value> {
852 struct Group { count: usize, nums: Vec<f64>, ints: Vec<i64>, all_int: bool }
862
863 let mut order: Vec<String> = vec![];
866 let mut groups: HashMap<String, Group> = HashMap::new();
867
868 const WHOLE: &str = "";
871
872 for node in rows {
873 let key = match spec.group_field {
874 None => WHOLE.to_string(),
875 Some(ref gf) => node.data.get(gf)
876 .map(as_text)
877 .unwrap_or_else(|| "null".to_string()),
878 };
879 let entry = groups.entry(key.clone()).or_insert_with(|| {
880 order.push(key.clone());
881 Group { count: 0, nums: vec![], ints: vec![], all_int: true }
882 });
883 entry.count += 1;
884 if let Some(ref af) = spec.agg_field {
885 match node.data.get(af) {
888 Some(Value::Number(n)) => {
889 if let Some(i) = n.as_i64() {
890 entry.ints.push(i);
891 entry.nums.push(i as f64);
892 } else if let Some(f) = n.as_f64() {
893 entry.all_int = false;
894 entry.nums.push(f);
895 }
896 }
897 _ => {}
898 }
899 }
900 }
901
902 if spec.group_field.is_none() && order.is_empty() {
906 order.push(WHOLE.to_string());
907 groups.insert(WHOLE.to_string(),
908 Group { count: 0, nums: vec![], ints: vec![], all_int: true });
909 }
910
911 order.into_iter().map(|k| {
912 let g = &groups[&k];
913 let mut obj = serde_json::Map::new();
914 if let Some(ref gf) = spec.group_field {
915 obj.insert(gf.clone(), Value::String(k.clone()));
916 }
917 obj.insert("count".to_string(), json!(g.count));
918
919 let int_path = g.all_int && !g.ints.is_empty();
923 let agg_val: Value = match spec.agg {
924 GroupAgg::Count => json!(g.count),
925 _ if g.nums.is_empty() => Value::Null,
926 GroupAgg::Sum if int_path => {
929 match g.ints.iter().try_fold(0i64, |a, &b| a.checked_add(b)) {
930 Some(t) => json!(t),
931 None => json!(g.nums.iter().sum::<f64>()),
932 }
933 }
934 GroupAgg::Min if int_path => json!(g.ints.iter().min().copied().unwrap()),
935 GroupAgg::Max if int_path => json!(g.ints.iter().max().copied().unwrap()),
936 GroupAgg::Sum => json!(g.nums.iter().sum::<f64>()),
937 GroupAgg::Avg => json!(g.nums.iter().sum::<f64>() / g.nums.len() as f64),
939 GroupAgg::Min => json!(g.nums.iter().cloned().fold(f64::INFINITY, f64::min)),
940 GroupAgg::Max => json!(g.nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max)),
941 };
942
943 if let Some(ref af) = spec.agg_field {
945 obj.insert(format!("{}_{}", spec.agg.name(), af), agg_val.clone());
946 }
947 obj.insert("value".to_string(), agg_val);
952 Value::Object(obj)
953 }).collect()
954}
955
956#[derive(Debug, Clone)]
965enum IndexPlan {
966 Range {
968 field: String,
969 low: Option<Value>,
970 high: Option<Value>,
971 low_incl: bool,
972 high_incl: bool,
973 },
974 Values { field: String, values: Vec<Value> },
976}
977
978impl IndexPlan {
979 fn field(&self) -> &str {
980 match self {
981 IndexPlan::Range { field, .. } => field,
982 IndexPlan::Values { field, .. } => field,
983 }
984 }
985}
986
987fn collect_index_constraints(pred: &Pred, out: &mut Vec<IndexPlan>) {
1000 match pred {
1001 Pred::And(terms) => {
1002 for t in terms {
1003 collect_index_constraints(t, out);
1004 }
1005 }
1006
1007 Pred::Cmp { field, op, value } => {
1008 if field == "_id" {
1010 return;
1011 }
1012 match op.as_str() {
1013 "=" => out.push(IndexPlan::Values {
1014 field: field.clone(),
1015 values: vec![value.clone()],
1016 }),
1017 ">" | ">=" => out.push(IndexPlan::Range {
1018 field: field.clone(),
1019 low: Some(value.clone()),
1020 high: None,
1021 low_incl: op == ">=",
1022 high_incl: true,
1023 }),
1024 "<" | "<=" => out.push(IndexPlan::Range {
1025 field: field.clone(),
1026 low: None,
1027 high: Some(value.clone()),
1028 low_incl: true,
1029 high_incl: op == "<=",
1030 }),
1031 _ => {}
1034 }
1035 }
1036
1037 Pred::Between { field, low, high, negated: false } => {
1038 out.push(IndexPlan::Range {
1039 field: field.clone(),
1040 low: Some(low.clone()),
1041 high: Some(high.clone()),
1042 low_incl: true, high_incl: true,
1044 });
1045 }
1046
1047 Pred::In { field, values, negated: false } => {
1048 out.push(IndexPlan::Values {
1049 field: field.clone(),
1050 values: values.clone(),
1051 });
1052 }
1053
1054 _ => {}
1060 }
1061}
1062
1063fn choose_index_plan(db: &Db, coll: &str, pred: &Pred) -> Option<IndexPlan> {
1070 let mut raw = vec![];
1071 collect_index_constraints(pred, &mut raw);
1072 raw.retain(|p| db.has_sorted_index(coll, p.field()));
1073 if raw.is_empty() {
1074 return None;
1075 }
1076
1077 let mut merged: Vec<IndexPlan> = vec![];
1079 for plan in raw {
1080 let field = plan.field().to_string();
1081 let existing = merged.iter().position(|m| m.field() == field);
1082 match (existing, plan) {
1083 (None, p) => merged.push(p),
1084
1085 (Some(i), IndexPlan::Range { low, high, low_incl, high_incl, .. }) => {
1087 if let IndexPlan::Range {
1088 low: ref mut elow, high: ref mut ehigh,
1089 low_incl: ref mut eli, high_incl: ref mut ehi, ..
1090 } = merged[i] {
1091 if let Some(l) = low {
1092 let tighter = match elow {
1093 None => true,
1094 Some(cur) => OrderedValue::from(&l) > OrderedValue::from(&*cur),
1095 };
1096 if tighter { *elow = Some(l); *eli = low_incl; }
1097 }
1098 if let Some(h) = high {
1099 let tighter = match ehigh {
1100 None => true,
1101 Some(cur) => OrderedValue::from(&h) < OrderedValue::from(&*cur),
1102 };
1103 if tighter { *ehigh = Some(h); *ehi = high_incl; }
1104 }
1105 }
1106 }
1110
1111 (Some(i), p @ IndexPlan::Values { .. }) => {
1113 if matches!(merged[i], IndexPlan::Range { .. }) {
1114 merged[i] = p;
1115 }
1116 }
1117 }
1118 }
1119
1120 let mut best: Option<(usize, IndexPlan)> = None;
1124 for plan in merged {
1125 let card = match &plan {
1126 IndexPlan::Range { field, low, high, low_incl, high_incl } => db
1127 .range_cardinality(coll, field, low.as_ref(), high.as_ref(),
1128 *low_incl, *high_incl)
1129 .unwrap_or(usize::MAX),
1130 IndexPlan::Values { field, values } => values
1131 .iter()
1132 .map(|v| db.range_cardinality(coll, field, Some(v), Some(v), true, true)
1133 .unwrap_or(usize::MAX))
1134 .fold(0usize, |a, b| a.saturating_add(b)),
1135 };
1136 if best.as_ref().map(|(c, _)| card < *c).unwrap_or(true) {
1137 best = Some((card, plan));
1138 }
1139 }
1140 best.map(|(_, p)| p)
1141}
1142
1143fn id_point_lookup(pred: &Pred) -> Option<String> {
1144 match pred {
1145 Pred::Cmp { field, op, value } if field == "_id" && op == "=" => {
1146 if let Value::String(s) = value { Some(s.clone()) } else { None }
1147 }
1148 Pred::And(terms) => terms.iter().find_map(id_point_lookup),
1149 _ => None,
1150 }
1151}
1152
1153fn matches_valid_as_of(node: &Node, date: &str) -> bool {
1154 let from_ok = node.valid_from.as_deref().map(|f| f <= date).unwrap_or(true);
1158 let to_ok = node.valid_to.as_deref().map(|t| t > date).unwrap_or(true);
1159 from_ok && to_ok
1160}
1161
1162fn node_contains_text(node: &Node, text: &str) -> bool {
1163 let s = node.data.to_string().to_lowercase();
1164 s.contains(&text.to_lowercase())
1165}
1166
1167pub fn node_to_json(node: &Node) -> Value {
1171 let mut obj = if let Value::Object(m) = &node.data {
1172 m.clone()
1173 } else {
1174 serde_json::Map::new()
1175 };
1176 obj.insert("_id".to_string(), Value::String(node.id.clone()));
1177 obj.insert("_hash".to_string(), Value::String(node.hash.clone()));
1178 obj.insert("_seq".to_string(), json!(node.seq));
1179 obj.insert("_coll".to_string(), Value::String(node.coll.clone()));
1180 if let Some(ref vf) = node.valid_from {
1181 obj.insert("_valid_from".to_string(), Value::String(vf.clone()));
1182 }
1183 if let Some(ref vt) = node.valid_to {
1184 obj.insert("_valid_to".to_string(), Value::String(vt.clone()));
1185 }
1186 if !node.caused_by.is_empty() {
1187 obj.insert("_caused_by".to_string(), Value::Array(
1188 node.caused_by.iter().map(|h| Value::String(h.clone())).collect()
1189 ));
1190 }
1191 Value::Object(obj)
1192}
1193
1194pub fn parse(nql: &str) -> Result<Query> {
1203 let mut lexer = Lexer::new(nql);
1204 let toks = lexer.tokenize();
1205 let mut parser = Parser::new(toks);
1206 parser.parse()
1207}
1208
1209pub fn execute(db: &Db, nql: &str) -> Result<Vec<Value>> {
1210 let q = parse(nql)?;
1213
1214 let id_eq_fast_path: Option<String> = if q.as_of.is_none() && q.trace.is_none() {
1221 q.where_.as_ref().and_then(id_point_lookup)
1222 } else { None };
1223
1224 let candidates: Vec<Node> = if let Some(ref target_id) = id_eq_fast_path {
1225 db.get(&q.coll, target_id).into_iter().collect()
1227 } else if let Some(seq_target) = q.as_of {
1228 db.id_index.list_ids(&q.coll).into_iter()
1230 .filter_map(|id| db.get_as_of(&q.coll, &id, seq_target))
1231 .collect()
1232 } else if let Some(plan) = q.where_.as_ref()
1233 .filter(|_| q.as_of.is_none())
1242 .and_then(|p| choose_index_plan(db, &q.coll, p))
1243 {
1244 let got = match &plan {
1247 IndexPlan::Range { field, low, high, low_incl, high_incl } => db.range_scan(
1248 &q.coll, field, low.as_ref(), high.as_ref(), *low_incl, *high_incl),
1249 IndexPlan::Values { field, values } => db.index_lookup(&q.coll, field, values),
1250 };
1251 match got {
1252 Some(nodes) => nodes,
1253 None => db.list(&q.coll),
1256 }
1257 } else if q.order_by.len() == 1 && q.aggregate.is_none() {
1258 let key = &q.order_by[0];
1273 let has_post_filters = q.where_.is_some()
1274 || q.search.is_some()
1275 || q.valid_as_of.is_some();
1276 let limit = if has_post_filters {
1277 9_999_999
1278 } else {
1279 match q.limit {
1283 Some(n) => n.saturating_add(q.offset.unwrap_or(0)),
1284 None => 9_999_999,
1285 }
1286 };
1287 if key.desc {
1288 db.order_by_desc(&q.coll, &key.field, limit)
1289 } else {
1290 db.order_by_asc(&q.coll, &key.field, limit)
1291 }
1292 } else if let (Some(n), true) = (q.limit, q.where_.is_none()
1293 && q.search.is_none() && q.trace.is_none()
1294 && q.traverse.is_none() && q.aggregate.is_none()
1295 && q.order_by.is_empty() && q.offset.is_none()
1296 && q.valid_as_of.is_none()) {
1297 db.id_index
1302 .list_ids(&q.coll)
1303 .into_iter()
1304 .take(n)
1305 .filter_map(|id| db.get(&q.coll, &id))
1306 .collect()
1307 } else {
1308 db.list(&q.coll)
1310 };
1311
1312 let mut rows: Vec<Node> = candidates.into_iter()
1315 .filter(|n| q.where_.as_ref().map(|p| eval_pred(n, p)).unwrap_or(true))
1316 .filter(|n| q.valid_as_of.as_deref()
1317 .map(|d| matches_valid_as_of(n, d))
1318 .unwrap_or(true))
1319 .filter(|n| q.search.as_deref()
1320 .map(|t| node_contains_text(n, t))
1321 .unwrap_or(true))
1322 .collect();
1323
1324 if let Some(ref _edge_type) = q.trace {
1327 let limit = q.limit.unwrap_or(1000);
1328 let mut traced: Vec<Node> = vec![];
1329 for root in &rows {
1330 let chain = db.trace(&root.hash, q.trace_rev, limit);
1331 traced.extend(chain);
1332 }
1333 rows = traced;
1334 }
1335
1336 if let Some(ref rel) = q.traverse {
1339 let mut traversed: Vec<Node> = vec![];
1340 for root in &rows {
1341 let frm = format!("{}:{}", root.coll, root.id);
1342 let neighbors = db.neighbors(&frm, rel);
1343 traversed.extend(neighbors);
1344 }
1345 rows = traversed;
1346 }
1347
1348 if let Some(ref spec) = q.aggregate {
1366 let mut out = aggregate_rows(&rows, spec);
1367
1368 if let Some(ref pred) = q.having {
1371 out.retain(|row| eval_pred_json(row, pred));
1372 }
1373
1374 if !q.order_by.is_empty() {
1375 sort_by_keys(&mut out, &q.order_by,
1376 |row, f| row.get(f).cloned().unwrap_or(Value::Null));
1377 } else if let Some(ref gf) = spec.group_field {
1378 let gf = gf.clone();
1386 out.sort_by(|a, b| {
1387 as_text(&a.get(&gf).cloned().unwrap_or(Value::Null))
1388 .cmp(&as_text(&b.get(&gf).cloned().unwrap_or(Value::Null)))
1389 });
1390 }
1391
1392 return Ok(paginate(out, q.offset, q.limit));
1393 }
1394
1395 if q.having.is_some() {
1396 bail!("HAVING requires an aggregate — add GROUP BY <field>, or use WHERE \
1397 to filter individual rows");
1398 }
1399
1400 if !q.order_by.is_empty() {
1403 let index_path_held = q.order_by.len() == 1
1407 && q.as_of.is_none()
1408 && q.where_.is_none()
1409 && q.search.is_none()
1410 && q.valid_as_of.is_none()
1411 && q.trace.is_none()
1412 && q.traverse.is_none();
1413 if !index_path_held {
1414 sort_by_keys(&mut rows, &q.order_by,
1415 |n, f| field_value(n, f));
1416 }
1417 }
1418
1419 let rows = paginate(rows, q.offset, q.limit);
1422
1423 Ok(rows.into_iter().map(|n| node_to_json(&n)).collect())
1426}
1427
1428pub fn query(db: &Db, nql: &str) -> Result<(Vec<Value>, usize)> {
1430 let rows = execute(db, nql)?;
1431 let count = rows.len();
1432 Ok((rows, count))
1433}
1434
1435#[cfg(test)]
1436mod tests {
1437 use super::*;
1438 use tempfile::tempdir;
1439 use crate::db::Db;
1440
1441 fn setup() -> (tempfile::TempDir, Db) {
1446 let dir = tempdir().unwrap();
1447 let db = Db::open(dir.path(), None).unwrap();
1448 db.create_sorted_index("blocks", "height");
1449 for h in 1u64..=5 {
1450 db.put("blocks", &h.to_string(),
1451 serde_json::json!({"height": h, "hash": format!("000{}", h), "n_tx": h * 2}),
1452 vec![], None, None).unwrap();
1453 }
1454 (dir, db)
1455 }
1456
1457 #[test]
1458 fn from_all() {
1459 let (_tmp, db) = setup();
1460 let (rows, count) = query(&db, "FROM blocks").unwrap();
1461 assert_eq!(count, 5);
1462 let _ = rows;
1463 }
1464
1465 #[test]
1466 fn where_eq() {
1467 let (_tmp, db) = setup();
1468 let (rows, count) = query(&db, r#"FROM blocks WHERE _id = "3""#).unwrap();
1469 assert_eq!(count, 1);
1470 assert_eq!(rows[0]["_id"], "3");
1471 }
1472
1473 #[test]
1474 fn order_by_limit() {
1475 let (_tmp, db) = setup();
1476 let (rows, count) = query(&db, "FROM blocks ORDER BY height ASC LIMIT 3").unwrap();
1477 assert_eq!(count, 3);
1478 assert_eq!(rows[0]["height"], 1);
1479 assert_eq!(rows[2]["height"], 3);
1480 }
1481
1482 #[test]
1483 fn order_by_desc() {
1484 let (_tmp, db) = setup();
1485 let (rows, _) = query(&db, "FROM blocks ORDER BY height DESC LIMIT 2").unwrap();
1486 assert_eq!(rows[0]["height"], 5);
1487 }
1488
1489 #[test]
1490 fn where_gt() {
1491 let (_tmp, db) = setup();
1492 let (rows, _) = query(&db, "FROM blocks WHERE height > 3").unwrap();
1493 assert_eq!(rows.len(), 2);
1494 }
1495
1496 #[test]
1503 fn where_order_limit_does_not_truncate_before_filter() {
1504 let (_tmp, db) = setup();
1505 let (rows, count) =
1506 query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height LIMIT 2").unwrap();
1507 assert_eq!(count, 2, "both matching rows must survive the limit");
1508 let heights: Vec<u64> = rows.iter()
1509 .filter_map(|r| r["height"].as_u64())
1510 .collect();
1511 assert_eq!(heights, vec![4, 5]);
1512 let (rows_d, _) =
1514 query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height DESC LIMIT 1").unwrap();
1515 assert_eq!(rows_d.len(), 1);
1516 assert_eq!(rows_d[0]["height"], 5);
1517 }
1518
1519 fn setup_text() -> (tempfile::TempDir, Db) {
1527 let dir = tempdir().unwrap();
1528 let db = Db::open(dir.path(), None).unwrap();
1529 let rows = [
1530 ("1", serde_json::json!({"status": "open", "miner": "Acme Pool", "fee": 10})),
1531 ("2", serde_json::json!({"status": "pending", "miner": "acme solo", "fee": 20})),
1532 ("3", serde_json::json!({"status": "closed", "miner": "Zenith", "fee": 30})),
1533 ("4", serde_json::json!({"status": "open", "fee": 40})),
1534 ("5", serde_json::json!({"status": "voided", "miner": Value::Null, "fee": 50})),
1535 ];
1536 for (id, data) in rows {
1537 db.put("jobs", id, data, vec![], None, None).unwrap();
1538 }
1539 (dir, db)
1540 }
1541
1542 fn ids(rows: &[Value]) -> Vec<String> {
1543 let mut v: Vec<String> = rows.iter()
1544 .filter_map(|r| r["_id"].as_str().map(String::from))
1545 .collect();
1546 v.sort();
1547 v
1548 }
1549
1550 #[test]
1551 fn where_in_list() {
1552 let (_tmp, db) = setup();
1553 let (rows, _) = query(&db, "FROM blocks WHERE height IN (2, 4)").unwrap();
1554 assert_eq!(ids(&rows), vec!["2", "4"]);
1555 }
1556
1557 #[test]
1558 fn where_in_strings() {
1559 let (_tmp, db) = setup_text();
1560 let (rows, _) = query(&db, r#"FROM jobs WHERE status IN ("open", "closed")"#).unwrap();
1561 assert_eq!(ids(&rows), vec!["1", "3", "4"]);
1562 }
1563
1564 #[test]
1565 fn where_not_in() {
1566 let (_tmp, db) = setup();
1567 let (rows, _) = query(&db, "FROM blocks WHERE height NOT IN (1, 2, 3)").unwrap();
1568 assert_eq!(ids(&rows), vec!["4", "5"]);
1569 }
1570
1571 #[test]
1572 fn where_in_single_value_equals_eq() {
1573 let (_tmp, db) = setup();
1574 let (a, _) = query(&db, "FROM blocks WHERE height IN (3)").unwrap();
1575 let (b, _) = query(&db, "FROM blocks WHERE height = 3").unwrap();
1576 assert_eq!(ids(&a), ids(&b));
1577 }
1578
1579 #[test]
1580 fn where_between_is_inclusive() {
1581 let (_tmp, db) = setup();
1582 let (rows, _) = query(&db, "FROM blocks WHERE height BETWEEN 2 AND 4").unwrap();
1583 assert_eq!(ids(&rows), vec!["2", "3", "4"]);
1585 }
1586
1587 #[test]
1588 fn where_not_between() {
1589 let (_tmp, db) = setup();
1590 let (rows, _) = query(&db, "FROM blocks WHERE height NOT BETWEEN 2 AND 4").unwrap();
1591 assert_eq!(ids(&rows), vec!["1", "5"]);
1592 }
1593
1594 #[test]
1598 fn between_and_does_not_swallow_the_conjunction() {
1599 let (_tmp, db) = setup();
1600 let (rows, _) = query(
1601 &db, "FROM blocks WHERE height BETWEEN 2 AND 4 AND n_tx > 4").unwrap();
1602 assert_eq!(ids(&rows), vec!["3", "4"]);
1604 }
1605
1606 #[test]
1607 fn where_like_prefix_suffix_and_infix() {
1608 let (_tmp, db) = setup_text();
1609 let (pre, _) = query(&db, r#"FROM jobs WHERE miner LIKE "Acme%""#).unwrap();
1610 assert_eq!(ids(&pre), vec!["1"]);
1611 let (suf, _) = query(&db, r#"FROM jobs WHERE miner LIKE "%Pool""#).unwrap();
1612 assert_eq!(ids(&suf), vec!["1"]);
1613 let (inf, _) = query(&db, r#"FROM jobs WHERE status LIKE "%pen%""#).unwrap();
1614 assert_eq!(ids(&inf), vec!["1", "2", "4"]); }
1616
1617 #[test]
1618 fn where_like_underscore_matches_exactly_one_char() {
1619 let (_tmp, db) = setup_text();
1620 let (rows, _) = query(&db, r#"FROM jobs WHERE status LIKE "open_""#).unwrap();
1621 assert!(rows.is_empty(), "`open_` must not match the 4-char value `open`");
1622 let (rows2, _) = query(&db, r#"FROM jobs WHERE status LIKE "ope_""#).unwrap();
1623 assert_eq!(ids(&rows2), vec!["1", "4"]);
1624 }
1625
1626 #[test]
1627 fn where_ilike_is_case_insensitive_and_like_is_not() {
1628 let (_tmp, db) = setup_text();
1629 let (ci, _) = query(&db, r#"FROM jobs WHERE miner ILIKE "acme%""#).unwrap();
1630 assert_eq!(ci.len(), 2, "ILIKE matches both `Acme Pool` and `acme solo`");
1631 let (cs, _) = query(&db, r#"FROM jobs WHERE miner LIKE "acme%""#).unwrap();
1632 assert_eq!(ids(&cs), vec!["2"], "LIKE stays case-sensitive");
1633 }
1634
1635 #[test]
1638 fn like_backtracks_across_multiple_wildcards() {
1639 assert!(like_match("abcabcabd", "%abc%abd", false));
1640 assert!(like_match("aaa", "%a", false));
1641 assert!(like_match("", "%", false));
1642 assert!(like_match("x", "%%%", false));
1643 assert!(!like_match("abc", "%abd", false));
1644 assert!(!like_match("ab", "ab_", false));
1645 assert!(like_match("héllo wörld", "h_llo w%d", false));
1646 }
1647
1648 #[test]
1649 fn where_not_like() {
1650 let (_tmp, db) = setup_text();
1651 let (rows, _) = query(&db, r#"FROM jobs WHERE status NOT LIKE "open""#).unwrap();
1652 assert_eq!(ids(&rows), vec!["2", "3", "5"]);
1653 }
1654
1655 #[test]
1659 fn like_over_null_is_false_in_both_polarities() {
1660 let (_tmp, db) = setup_text();
1661 let (pos, _) = query(&db, r#"FROM jobs WHERE miner LIKE "%""#).unwrap();
1662 let (neg, _) = query(&db, r#"FROM jobs WHERE miner NOT LIKE "%""#).unwrap();
1663 assert!(!ids(&pos).contains(&"4".to_string()));
1664 assert!(!ids(&neg).contains(&"4".to_string()));
1665 assert!(!ids(&pos).contains(&"5".to_string()));
1666 assert!(!ids(&neg).contains(&"5".to_string()));
1667 }
1668
1669 #[test]
1672 fn where_is_null_catches_absent_and_explicit_null() {
1673 let (_tmp, db) = setup_text();
1674 let (rows, _) = query(&db, "FROM jobs WHERE miner IS NULL").unwrap();
1675 assert_eq!(ids(&rows), vec!["4", "5"]);
1676 }
1677
1678 #[test]
1679 fn where_is_not_null() {
1680 let (_tmp, db) = setup_text();
1681 let (rows, _) = query(&db, "FROM jobs WHERE miner IS NOT NULL").unwrap();
1682 assert_eq!(ids(&rows), vec!["1", "2", "3"]);
1683 }
1684
1685 #[test]
1686 fn where_or() {
1687 let (_tmp, db) = setup();
1688 let (rows, _) = query(&db, "FROM blocks WHERE height = 1 OR height = 5").unwrap();
1689 assert_eq!(ids(&rows), vec!["1", "5"]);
1690 }
1691
1692 #[test]
1695 fn and_binds_tighter_than_or() {
1696 let (_tmp, db) = setup();
1697 let (rows, _) = query(
1698 &db, "FROM blocks WHERE height = 1 OR height = 5 AND n_tx = 10").unwrap();
1699 assert_eq!(ids(&rows), vec!["1", "5"]);
1700 let (rows2, _) = query(
1701 &db, "FROM blocks WHERE height = 1 OR height = 5 AND n_tx = 99").unwrap();
1702 assert_eq!(ids(&rows2), vec!["1"], "the AND arm must not match");
1703 }
1704
1705 #[test]
1707 fn parens_override_precedence() {
1708 let (_tmp, db) = setup();
1709 let (rows, _) = query(
1710 &db, "FROM blocks WHERE (height = 1 OR height = 5) AND n_tx = 10").unwrap();
1711 assert_eq!(ids(&rows), vec!["5"]);
1712 }
1713
1714 #[test]
1715 fn nested_parens() {
1716 let (_tmp, db) = setup();
1717 let (rows, _) = query(
1718 &db,
1719 "FROM blocks WHERE ((height >= 2 AND height <= 4) OR height = 1) AND n_tx != 6",
1720 ).unwrap();
1721 assert_eq!(ids(&rows), vec!["1", "2", "4"]);
1722 }
1723
1724 #[test]
1725 fn not_negates_a_group() {
1726 let (_tmp, db) = setup();
1727 let (rows, _) = query(&db, "FROM blocks WHERE NOT (height > 2)").unwrap();
1728 assert_eq!(ids(&rows), vec!["1", "2"]);
1729 }
1730
1731 #[test]
1735 fn prefix_not_before_a_comparison() {
1736 let (_tmp, db) = setup();
1737 let (rows, _) = query(&db, "FROM blocks WHERE NOT height = 1").unwrap();
1738 assert_eq!(ids(&rows), vec!["2", "3", "4", "5"]);
1739 let (double, _) = query(&db, "FROM blocks WHERE NOT NOT height = 1").unwrap();
1740 assert_eq!(ids(&double), vec!["1"]);
1741 let (mixed, _) = query(&db, "FROM blocks WHERE NOT height = 1 AND height < 4").unwrap();
1742 assert_eq!(ids(&mixed), vec!["2", "3"]);
1743 }
1744
1745 #[test]
1749 fn id_equality_under_or_does_not_become_a_point_lookup() {
1750 let (_tmp, db) = setup();
1751 let (rows, _) = query(&db, r#"FROM blocks WHERE _id = "1" OR height > 3"#).unwrap();
1752 assert_eq!(ids(&rows), vec!["1", "4", "5"],
1753 "the OR arm must survive the id fast path");
1754 }
1755
1756 #[test]
1758 fn id_equality_under_and_still_point_looks_up() {
1759 let (_tmp, db) = setup();
1760 let (hit, _) = query(&db, r#"FROM blocks WHERE _id = "3" AND n_tx = 6"#).unwrap();
1761 assert_eq!(ids(&hit), vec!["3"]);
1762 let (miss, _) = query(&db, r#"FROM blocks WHERE _id = "3" AND n_tx = 999"#).unwrap();
1763 assert!(miss.is_empty(), "the second conjunct must still be applied");
1764 }
1765
1766 #[test]
1767 fn metadata_fields_are_filterable() {
1768 let (_tmp, db) = setup();
1769 let (rows, _) = query(&db, "FROM blocks WHERE _seq >= 0 AND _coll = blocks").unwrap();
1771 assert_eq!(rows.len(), 5);
1772 let (tail, _) = query(&db, "FROM blocks WHERE _seq > 0").unwrap();
1773 assert_eq!(tail.len(), 4);
1774 }
1775
1776 #[test]
1777 fn combined_with_order_and_limit() {
1778 let (_tmp, db) = setup();
1779 let (rows, _) = query(
1780 &db,
1781 "FROM blocks WHERE height IN (1, 3, 5) ORDER BY height DESC LIMIT 2",
1782 ).unwrap();
1783 let heights: Vec<u64> = rows.iter().filter_map(|r| r["height"].as_u64()).collect();
1784 assert_eq!(heights, vec![5, 3]);
1785 }
1786
1787 #[test]
1793 fn unknown_clauses_are_errors_not_silent_skips() {
1794 let (_tmp, db) = setup();
1795 for bad in [
1796 "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,", ] {
1804 assert!(query(&db, bad).is_err(), "`{}` must be rejected, not silently reinterpreted", bad);
1805 }
1806 }
1807
1808 #[test]
1809 fn malformed_predicates_are_errors() {
1810 let (_tmp, db) = setup();
1811 for bad in [
1812 "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", ] {
1822 assert!(query(&db, bad).is_err(), "`{}` must be a parse error", bad);
1823 }
1824 }
1825
1826 #[test]
1830 fn asc_is_accepted_explicitly() {
1831 let (_tmp, db) = setup();
1832 let (asc, _) = query(&db, "FROM blocks ORDER BY height ASC").unwrap();
1833 let (plain, _) = query(&db, "FROM blocks ORDER BY height").unwrap();
1834 assert_eq!(asc[0]["height"], 1);
1835 assert_eq!(plain[0]["height"], 1);
1836 }
1837
1838 #[test]
1841 fn new_keywords_are_case_insensitive() {
1842 let (_tmp, db) = setup();
1843 let (rows, _) = query(&db, "from blocks where height between 2 and 3").unwrap();
1844 assert_eq!(ids(&rows), vec!["2", "3"]);
1845 let (rows2, _) = query(&db, "FROM blocks Where height In (1) Or height In (2)").unwrap();
1846 assert_eq!(ids(&rows2), vec!["1", "2"]);
1847 }
1848
1849 #[test]
1850 fn group_by_count() {
1851 let (_tmp, db) = setup();
1852 let (rows, _) = query(&db, "FROM blocks GROUP BY n_tx COUNT").unwrap();
1853 assert_eq!(rows.len(), 5); }
1855
1856 fn twin(fields: &[&str]) -> (tempfile::TempDir, tempfile::TempDir, Db, Db) {
1868 let d1 = tempdir().unwrap();
1869 let d2 = tempdir().unwrap();
1870 let indexed = Db::open(d1.path(), None).unwrap();
1871 let plain = Db::open(d2.path(), None).unwrap();
1872 for f in fields {
1873 indexed.create_sorted_index("t", f);
1874 }
1875 let rows: Vec<(String, Value)> = (0..40u64).map(|i| {
1878 let mut o = serde_json::Map::new();
1879 if i % 7 != 0 {
1880 o.insert("fee".into(), json!(i % 13));
1881 }
1882 if i % 11 == 0 {
1883 o.insert("note".into(), Value::Null);
1884 } else {
1885 o.insert("note".into(), json!(format!("n{}", i % 5)));
1886 }
1887 o.insert("rank".into(), json!(40 - i));
1888 (i.to_string(), Value::Object(o))
1889 }).collect();
1890 for (id, doc) in &rows {
1891 indexed.put("t", id, doc.clone(), vec![], None, None).unwrap();
1892 plain.put("t", id, doc.clone(), vec![], None, None).unwrap();
1893 }
1894 (d1, d2, indexed, plain)
1895 }
1896
1897 fn same(a: &Db, b: &Db, nql: &str) -> (Vec<String>, Vec<String>) {
1898 let ga = {
1899 let (rows, _) = query(a, nql).unwrap();
1900 let mut v: Vec<String> = rows.iter()
1901 .filter_map(|r| r["_id"].as_str().map(String::from)).collect();
1902 v.sort(); v
1903 };
1904 let gb = {
1905 let (rows, _) = query(b, nql).unwrap();
1906 let mut v: Vec<String> = rows.iter()
1907 .filter_map(|r| r["_id"].as_str().map(String::from)).collect();
1908 v.sort(); v
1909 };
1910 (ga, gb)
1911 }
1912
1913 #[test]
1914 fn indexed_and_unindexed_agree_on_every_predicate_shape() {
1915 let (_t1, _t2, idx, plain) = twin(&["fee", "note", "rank"]);
1916 for nql in [
1917 "FROM t WHERE fee > 5",
1919 "FROM t WHERE fee >= 5",
1920 "FROM t WHERE fee < 5",
1921 "FROM t WHERE fee <= 5",
1922 "FROM t WHERE fee = 5",
1923 "FROM t WHERE fee BETWEEN 3 AND 8",
1924 "FROM t WHERE fee NOT BETWEEN 3 AND 8",
1925 "FROM t WHERE fee IN (1, 5, 9)",
1926 "FROM t WHERE fee NOT IN (1, 5, 9)",
1927 "FROM t WHERE fee != 5",
1928 "FROM t WHERE fee > 3 AND fee < 9",
1930 "FROM t WHERE fee >= 3 AND fee <= 9",
1931 "FROM t WHERE fee > 3 AND fee < 9 AND fee != 5",
1932 "FROM t WHERE fee BETWEEN 2 AND 10 AND fee > 6",
1933 "FROM t WHERE fee > 5 AND rank < 20",
1935 "FROM t WHERE fee IN (2, 3) AND rank > 10",
1936 "FROM t WHERE fee = 4 AND rank = 8",
1937 "FROM t WHERE fee IS NULL",
1939 "FROM t WHERE fee IS NOT NULL",
1940 "FROM t WHERE note IS NULL",
1941 "FROM t WHERE note IS NOT NULL",
1942 "FROM t WHERE fee IS NULL AND rank > 20",
1943 r#"FROM t WHERE note LIKE "n_""#,
1945 r#"FROM t WHERE fee > 5 AND note LIKE "n1""#,
1946 r#"FROM t WHERE note NOT LIKE "n1" AND fee < 4"#,
1947 "FROM t WHERE fee > 11 OR rank > 38",
1949 "FROM t WHERE fee = 1 OR note IS NULL",
1950 "FROM t WHERE (fee > 11 OR rank > 38) AND rank < 39",
1951 "FROM t WHERE fee IN (1) OR fee IN (2)",
1952 "FROM t WHERE NOT (fee > 5)",
1954 "FROM t WHERE NOT (fee IN (1, 2))",
1955 "FROM t WHERE NOT (fee > 5) AND rank < 30",
1956 "FROM t WHERE fee > 5 ORDER BY rank DESC LIMIT 5",
1958 "FROM t WHERE fee BETWEEN 2 AND 8 ORDER BY fee, rank DESC",
1959 "FROM t WHERE fee > 5 GROUP BY note COUNT",
1960 "FROM t WHERE fee > 5 COUNT",
1961 "FROM t WHERE fee > 5 ORDER BY rank LIMIT 3 OFFSET 2",
1962 "FROM t WHERE fee > 9999",
1964 "FROM t WHERE fee IN (9999)",
1965 "FROM t WHERE fee BETWEEN 100 AND 200",
1966 ] {
1967 let (a, b) = same(&idx, &plain, nql);
1968 assert_eq!(a, b, "indexed and unindexed disagree on `{}`", nql);
1969 }
1970 }
1971
1972 #[test]
1976 fn index_path_still_honours_order_by() {
1977 let (_t1, _t2, idx, plain) = twin(&["fee", "rank"]);
1978 for nql in [
1979 "FROM t WHERE fee > 4 ORDER BY rank",
1980 "FROM t WHERE fee > 4 ORDER BY rank DESC",
1981 "FROM t WHERE fee > 4 ORDER BY note, rank DESC",
1982 "FROM t WHERE fee BETWEEN 2 AND 9 ORDER BY rank LIMIT 4",
1983 "FROM t WHERE fee IN (3, 6) ORDER BY rank DESC LIMIT 2",
1984 ] {
1985 let ra = query(&idx, nql).unwrap().0;
1986 let rb = query(&plain, nql).unwrap().0;
1987 let ia: Vec<&str> = ra.iter().filter_map(|r| r["_id"].as_str()).collect();
1988 let ib: Vec<&str> = rb.iter().filter_map(|r| r["_id"].as_str()).collect();
1989 assert_eq!(ia, ib, "row ORDER differs on `{}`", nql);
1990 }
1991 }
1992
1993 #[test]
2002 fn an_ordering_comparison_against_a_missing_field_is_false() {
2003 let dir = tempdir().unwrap();
2004 let db = Db::open(dir.path(), None).unwrap();
2005 db.put("t", "has", json!({"fee": 1}), vec![], None, None).unwrap();
2006 db.put("t", "none", json!({"other": 1}), vec![], None, None).unwrap();
2007 db.put("t", "null", json!({"fee": Value::Null}), vec![], None, None).unwrap();
2008
2009 for nql in ["FROM t WHERE fee < 5", "FROM t WHERE fee <= 5"] {
2010 let (r, _) = query(&db, nql).unwrap();
2011 let ids: Vec<&str> = r.iter().filter_map(|x| x["_id"].as_str()).collect();
2012 assert_eq!(ids, vec!["has"],
2013 "`{}` must not match a row whose fee is absent or null", nql);
2014 }
2015 for nql in ["FROM t WHERE fee > 0", "FROM t WHERE fee >= 0"] {
2016 let (r, _) = query(&db, nql).unwrap();
2017 let ids: Vec<&str> = r.iter().filter_map(|x| x["_id"].as_str()).collect();
2018 assert_eq!(ids, vec!["has"], "`{}`", nql);
2019 }
2020 let (b, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 9").unwrap();
2022 assert_eq!(b.len(), 1);
2023
2024 let (ne, _) = query(&db, "FROM t WHERE fee != 5").unwrap();
2027 assert_eq!(ne.len(), 3, "!= still matches absent and null fields");
2028 let (isnull, _) = query(&db, "FROM t WHERE fee = NULL").unwrap();
2029 assert_eq!(isnull.len(), 2, "absent and explicit-null both equal NULL");
2030
2031 let d2 = tempdir().unwrap();
2034 let idx = Db::open(d2.path(), None).unwrap();
2035 idx.create_sorted_index("t", "fee");
2036 idx.put("t", "has", json!({"fee": 1}), vec![], None, None).unwrap();
2037 idx.put("t", "none", json!({"other": 1}), vec![], None, None).unwrap();
2038 idx.put("t", "null", json!({"fee": Value::Null}), vec![], None, None).unwrap();
2039 for nql in ["FROM t WHERE fee < 5", "FROM t WHERE fee <= 5",
2040 "FROM t WHERE fee > 0", "FROM t WHERE fee BETWEEN 0 AND 9"] {
2041 let (a, _) = query(&db, nql).unwrap();
2042 let (b, _) = query(&idx, nql).unwrap();
2043 let ia: Vec<&str> = a.iter().filter_map(|x| x["_id"].as_str()).collect();
2044 let ib: Vec<&str> = b.iter().filter_map(|x| x["_id"].as_str()).collect();
2045 assert_eq!(ia, ib, "indexed and unindexed disagree on `{}`", nql);
2046 }
2047 }
2048
2049 #[test]
2054 fn is_null_never_uses_the_index() {
2055 let (_t1, _t2, idx, plain) = twin(&["fee"]);
2056 let (a, b) = same(&idx, &plain, "FROM t WHERE fee IS NULL");
2057 assert_eq!(a, b);
2058 assert_eq!(a, vec!["0", "14", "21", "28", "35", "7"]);
2060 assert!(!a.is_empty(), "the fixture must actually contain absent fields");
2061 }
2062
2063 #[test]
2066 fn a_disjunct_is_never_used_to_narrow() {
2067 let (_t1, _t2, idx, plain) = twin(&["fee", "rank"]);
2068 let nql = "FROM t WHERE fee = 1 OR rank = 40";
2069 let (a, b) = same(&idx, &plain, nql);
2070 assert_eq!(a, b);
2071 assert!(a.contains(&"0".to_string()),
2074 "the OR arm matching a doc with no indexed field must survive: {:?}", a);
2075 assert!(a.len() > 1, "both arms must contribute: {:?}", a);
2076 }
2077
2078 #[test]
2082 fn as_of_does_not_use_the_current_version_index() {
2083 let dir = tempdir().unwrap();
2084 let db = Db::open(dir.path(), None).unwrap();
2085 db.create_sorted_index("t", "fee");
2086 db.put("t", "a", json!({"fee": 5}), vec![], None, None).unwrap();
2087 let snap = db.put("t", "b", json!({"fee": 5}), vec![], None, None).unwrap().seq;
2088 db.put("t", "a", json!({"fee": 999}), vec![], None, None).unwrap();
2090 db.put("t", "b", json!({"fee": 999}), vec![], None, None).unwrap();
2091
2092 let (now, _) = query(&db, "FROM t WHERE fee = 5").unwrap();
2094 assert!(now.is_empty(), "current versions have fee 999: {:?}", now);
2095
2096 let (then, _) = query(&db, &format!("FROM t AS OF {} WHERE fee = 5", snap)).unwrap();
2099 let mut ids: Vec<&str> = then.iter().filter_map(|r| r["_id"].as_str()).collect();
2100 ids.sort();
2101 assert_eq!(ids, vec!["a", "b"], "AS OF must see the historical values");
2102
2103 let (r, _) = query(&db, &format!("FROM t AS OF {} WHERE fee BETWEEN 1 AND 9", snap)).unwrap();
2105 assert_eq!(r.len(), 2);
2106 let (i, _) = query(&db, &format!("FROM t AS OF {} WHERE fee IN (5)", snap)).unwrap();
2107 assert_eq!(i.len(), 2);
2108 }
2109
2110 #[test]
2112 fn the_index_path_returns_current_versions_only() {
2113 let dir = tempdir().unwrap();
2114 let db = Db::open(dir.path(), None).unwrap();
2115 db.create_sorted_index("t", "fee");
2116 for i in 0..5u64 {
2117 db.put("t", &i.to_string(), json!({"fee": i}), vec![], None, None).unwrap();
2118 }
2119 db.put("t", "0", json!({"fee": 100}), vec![], None, None).unwrap();
2120
2121 let (low, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 4").unwrap();
2122 let mut ids: Vec<&str> = low.iter().filter_map(|r| r["_id"].as_str()).collect();
2123 ids.sort();
2124 assert_eq!(ids, vec!["1", "2", "3", "4"],
2125 "doc 0 moved to fee 100 and must not appear in 0..4");
2126
2127 let (high, _) = query(&db, "FROM t WHERE fee = 100").unwrap();
2128 assert_eq!(high.len(), 1);
2129 assert_eq!(high[0]["_id"], "0");
2130 assert_eq!(high[0]["fee"], json!(100), "the CURRENT value, not the old one");
2131 }
2132
2133 #[test]
2136 fn index_scans_do_not_duplicate_rows() {
2137 let dir = tempdir().unwrap();
2138 let db = Db::open(dir.path(), None).unwrap();
2139 db.create_sorted_index("t", "fee");
2140 for i in 0..6u64 {
2141 db.put("t", &i.to_string(), json!({"fee": i % 2}), vec![], None, None).unwrap();
2142 }
2143 let (dup, _) = query(&db, "FROM t WHERE fee IN (0, 0, 1, 1)").unwrap();
2144 assert_eq!(dup.len(), 6, "each row once despite repeated IN arms");
2145 let (r, _) = query(&db, "FROM t WHERE fee BETWEEN 0 AND 1").unwrap();
2146 assert_eq!(r.len(), 6);
2147 let mut ids: Vec<&str> = dup.iter().filter_map(|r| r["_id"].as_str()).collect();
2148 ids.sort();
2149 ids.dedup();
2150 assert_eq!(ids.len(), 6, "no duplicate _ids");
2151 }
2152
2153 #[test]
2155 fn index_ranges_work_on_strings() {
2156 let dir = tempdir().unwrap();
2157 let db = Db::open(dir.path(), None).unwrap();
2158 db.create_sorted_index("t", "name");
2159 for (i, n) in ["alpha", "bravo", "charlie", "delta", "echo"].iter().enumerate() {
2160 db.put("t", &i.to_string(), json!({"name": n}), vec![], None, None).unwrap();
2161 }
2162 let (r, _) = query(&db, r#"FROM t WHERE name BETWEEN "bravo" AND "delta""#).unwrap();
2163 let mut got: Vec<&str> = r.iter().filter_map(|x| x["name"].as_str()).collect();
2164 got.sort();
2165 assert_eq!(got, vec!["bravo", "charlie", "delta"]);
2166 let (gt, _) = query(&db, r#"FROM t WHERE name > "charlie""#).unwrap();
2167 assert_eq!(gt.len(), 2);
2168 }
2169
2170 #[test]
2173 fn same_field_bounds_are_merged_tightest_wins() {
2174 let (_t1, _t2, idx, plain) = twin(&["fee"]);
2175 for (a_nql, b_nql) in [
2176 ("FROM t WHERE fee > 2 AND fee > 6", "FROM t WHERE fee > 6"),
2177 ("FROM t WHERE fee > 6 AND fee > 2", "FROM t WHERE fee > 6"),
2178 ("FROM t WHERE fee < 9 AND fee < 4", "FROM t WHERE fee < 4"),
2179 ("FROM t WHERE fee BETWEEN 0 AND 12 AND fee >= 5 AND fee <= 7",
2180 "FROM t WHERE fee >= 5 AND fee <= 7"),
2181 ] {
2182 let (ia, _) = same(&idx, &plain, a_nql);
2183 let (ib, _) = same(&idx, &plain, b_nql);
2184 assert_eq!(ia, ib, "`{}` should equal `{}`", a_nql, b_nql);
2185 }
2186 }
2187
2188 #[test]
2191 fn a_predicate_on_an_unindexed_field_still_answers() {
2192 let (_t1, _t2, idx, plain) = twin(&["fee"]); for nql in [
2194 "FROM t WHERE rank > 30",
2195 "FROM t WHERE rank BETWEEN 10 AND 20",
2196 "FROM t WHERE rank IN (40, 39)",
2197 "FROM t WHERE rank > 30 AND fee > 2",
2198 ] {
2199 let (a, b) = same(&idx, &plain, nql);
2200 assert_eq!(a, b, "`{}`", nql);
2201 }
2202 }
2203
2204 #[test]
2207 fn range_cardinality_counts_without_reading() {
2208 let dir = tempdir().unwrap();
2209 let db = Db::open(dir.path(), None).unwrap();
2210 db.create_sorted_index("t", "fee");
2211 for i in 0..20u64 {
2212 db.put("t", &i.to_string(), json!({"fee": i}), vec![], None, None).unwrap();
2213 }
2214 assert_eq!(db.range_cardinality("t", "fee", None, None, true, true), Some(20));
2215 assert_eq!(
2216 db.range_cardinality("t", "fee", Some(&json!(5)), Some(&json!(9)), true, true),
2217 Some(5), "5..=9 inclusive is five values");
2218 assert_eq!(
2219 db.range_cardinality("t", "fee", Some(&json!(5)), Some(&json!(9)), false, false),
2220 Some(3), "exclusive bounds drop both ends");
2221 assert_eq!(
2222 db.range_cardinality("t", "fee", Some(&json!(18)), None, true, true),
2223 Some(2));
2224 assert_eq!(
2225 db.range_cardinality("t", "fee", Some(&json!(999)), None, true, true),
2226 Some(0), "an empty range is 0, not an error");
2227 assert_eq!(db.range_cardinality("t", "nope", None, None, true, true), None);
2229 }
2230
2231 #[test]
2235 fn the_narrower_index_is_preferred() {
2236 let dir = tempdir().unwrap();
2237 let db = Db::open(dir.path(), None).unwrap();
2238 db.create_sorted_index("t", "wide");
2239 db.create_sorted_index("t", "narrow");
2240 for i in 0..100u64 {
2241 db.put("t", &i.to_string(),
2242 json!({"wide": i % 2, "narrow": i}), vec![], None, None).unwrap();
2243 }
2244 let wide = db.range_cardinality("t", "wide", Some(&json!(0)), Some(&json!(0)), true, true);
2246 let narrow = db.range_cardinality("t", "narrow", Some(&json!(7)), Some(&json!(7)), true, true);
2247 assert_eq!(wide, Some(50));
2248 assert_eq!(narrow, Some(1));
2249 let (r, _) = query(&db, "FROM t WHERE wide = 0 AND narrow = 7").unwrap();
2251 assert!(r.is_empty(), "narrow 7 has wide 1, so nothing matches");
2252 let (r2, _) = query(&db, "FROM t WHERE wide = 0 AND narrow = 8").unwrap();
2253 assert_eq!(r2.len(), 1);
2254 assert_eq!(r2[0]["_id"], "8");
2255 }
2256
2257 fn heights(rows: &[Value]) -> Vec<u64> {
2261 rows.iter().filter_map(|r| r["height"].as_u64()).collect()
2262 }
2263
2264 #[test]
2265 fn offset_skips_result_rows() {
2266 let (_tmp, db) = setup();
2267 let (rows, _) = query(&db, "FROM blocks ORDER BY height OFFSET 2").unwrap();
2268 assert_eq!(heights(&rows), vec![3, 4, 5]);
2269 }
2270
2271 #[test]
2272 fn offset_with_limit_pages() {
2273 let (_tmp, db) = setup();
2274 let mut seen = vec![];
2278 for page in 0..3 {
2279 let (rows, _) = query(
2280 &db,
2281 &format!("FROM blocks ORDER BY height LIMIT 2 OFFSET {}", page * 2),
2282 ).unwrap();
2283 seen.extend(heights(&rows));
2284 }
2285 assert_eq!(seen, vec![1, 2, 3, 4, 5]);
2286 }
2287
2288 #[test]
2289 fn offset_past_the_end_is_an_empty_page() {
2290 let (_tmp, db) = setup();
2291 let (rows, count) = query(&db, "FROM blocks OFFSET 99").unwrap();
2292 assert!(rows.is_empty());
2293 assert_eq!(count, 0);
2294 let (zero, _) = query(&db, "FROM blocks OFFSET 0").unwrap();
2295 assert_eq!(zero.len(), 5, "OFFSET 0 skips nothing");
2296 }
2297
2298 #[test]
2299 fn offset_applies_after_the_filter() {
2300 let (_tmp, db) = setup();
2301 let (rows, _) = query(
2304 &db, "FROM blocks WHERE n_tx >= 6 ORDER BY height OFFSET 1").unwrap();
2305 assert_eq!(heights(&rows), vec![4, 5]);
2306 }
2307
2308 #[test]
2309 fn order_by_multiple_keys() {
2310 let dir = tempdir().unwrap();
2311 let db = Db::open(dir.path(), None).unwrap();
2312 for (i, (s, f)) in [("open", 30), ("open", 10), ("closed", 20),
2315 ("open", 20), ("closed", 5)].iter().enumerate() {
2316 db.put("t", &i.to_string(),
2317 serde_json::json!({"status": s, "fee": f}), vec![], None, None).unwrap();
2318 }
2319 let (rows, _) = query(&db, "FROM t ORDER BY status, fee DESC").unwrap();
2320 let got: Vec<(String, u64)> = rows.iter()
2321 .map(|r| (r["status"].as_str().unwrap().to_string(), r["fee"].as_u64().unwrap()))
2322 .collect();
2323 assert_eq!(got, vec![
2324 ("closed".into(), 20), ("closed".into(), 5),
2325 ("open".into(), 30), ("open".into(), 20), ("open".into(), 10),
2326 ]);
2327 }
2328
2329 #[test]
2330 fn order_by_mixed_directions() {
2331 let dir = tempdir().unwrap();
2332 let db = Db::open(dir.path(), None).unwrap();
2333 for (i, (a, b)) in [(1, 1), (1, 2), (2, 1), (2, 2)].iter().enumerate() {
2334 db.put("t", &i.to_string(),
2335 serde_json::json!({"a": a, "b": b}), vec![], None, None).unwrap();
2336 }
2337 let (rows, _) = query(&db, "FROM t ORDER BY a DESC, b ASC").unwrap();
2338 let got: Vec<(u64, u64)> = rows.iter()
2339 .map(|r| (r["a"].as_u64().unwrap(), r["b"].as_u64().unwrap()))
2340 .collect();
2341 assert_eq!(got, vec![(2, 1), (2, 2), (1, 1), (1, 2)]);
2342 }
2343
2344 #[test]
2349 fn limit_applies_to_grouped_rows_not_to_the_input() {
2350 let dir = tempdir().unwrap();
2351 let db = Db::open(dir.path(), None).unwrap();
2352 for i in 0..12 {
2353 let status = ["open", "closed", "void"][i % 3];
2355 db.put("t", &i.to_string(),
2356 serde_json::json!({"status": status, "fee": i}),
2357 vec![], None, None).unwrap();
2358 }
2359 let (all, _) = query(&db, "FROM t GROUP BY status COUNT").unwrap();
2360 assert_eq!(all.len(), 3);
2361 let total: u64 = all.iter().filter_map(|r| r["count"].as_u64()).sum();
2362 assert_eq!(total, 12, "every input row must be counted");
2363
2364 let (limited, _) = query(&db, "FROM t GROUP BY status COUNT LIMIT 2").unwrap();
2367 assert_eq!(limited.len(), 2, "LIMIT caps the number of groups");
2368 for r in &limited {
2369 assert_eq!(r["count"], json!(4),
2370 "each group keeps its true count, got {:?}", r);
2371 }
2372 }
2373
2374 #[test]
2379 fn order_by_sorts_the_grouped_rows() {
2380 let dir = tempdir().unwrap();
2381 let db = Db::open(dir.path(), None).unwrap();
2382 for (i, s) in ["a", "b", "b", "b", "c", "c"].iter().enumerate() {
2384 db.put("t", &i.to_string(),
2385 serde_json::json!({"g": s, "n": i}), vec![], None, None).unwrap();
2386 }
2387 let (rows, _) = query(&db, "FROM t GROUP BY g COUNT ORDER BY count DESC").unwrap();
2388 let got: Vec<(String, u64)> = rows.iter()
2389 .map(|r| (r["g"].as_str().unwrap().to_string(), r["count"].as_u64().unwrap()))
2390 .collect();
2391 assert_eq!(got, vec![("b".into(), 3), ("c".into(), 2), ("a".into(), 1)]);
2392
2393 let (by_key, _) = query(&db, "FROM t GROUP BY g COUNT ORDER BY g DESC").unwrap();
2395 let keys: Vec<&str> = by_key.iter().map(|r| r["g"].as_str().unwrap()).collect();
2396 assert_eq!(keys, vec!["c", "b", "a"]);
2397 }
2398
2399 #[test]
2400 fn order_by_an_aggregate_key() {
2401 let (_tmp, db) = setup_items();
2402 let (rows, _) = query(
2403 &db, "FROM items GROUP BY cat SUM price ORDER BY sum_price DESC").unwrap();
2404 let cats: Vec<&str> = rows.iter().map(|r| r["cat"].as_str().unwrap()).collect();
2405 assert_eq!(cats, vec!["y", "x"], "y sums to 60, x to 15");
2406 }
2407
2408 #[test]
2409 fn offset_and_limit_page_grouped_rows() {
2410 let dir = tempdir().unwrap();
2411 let db = Db::open(dir.path(), None).unwrap();
2412 for i in 0..9 {
2413 db.put("t", &i.to_string(),
2414 serde_json::json!({"g": format!("g{}", i % 3)}), vec![], None, None).unwrap();
2415 }
2416 let (page, _) = query(
2417 &db, "FROM t GROUP BY g COUNT ORDER BY g LIMIT 1 OFFSET 1").unwrap();
2418 assert_eq!(page.len(), 1);
2419 assert_eq!(page[0]["g"], "g1");
2420 }
2421
2422 #[test]
2425 fn having_filters_groups_by_count() {
2426 let dir = tempdir().unwrap();
2427 let db = Db::open(dir.path(), None).unwrap();
2428 for (i, s) in ["a", "b", "b", "b", "c", "c"].iter().enumerate() {
2429 db.put("t", &i.to_string(),
2430 serde_json::json!({"g": s, "n": i}), vec![], None, None).unwrap();
2431 }
2432 let (rows, _) = query(&db, "FROM t GROUP BY g COUNT HAVING count > 1").unwrap();
2433 let mut keys: Vec<&str> = rows.iter().map(|r| r["g"].as_str().unwrap()).collect();
2434 keys.sort();
2435 assert_eq!(keys, vec!["b", "c"], "the single-row group `a` is filtered out");
2436 }
2437
2438 #[test]
2439 fn having_filters_on_the_aggregate_value() {
2440 let (_tmp, db) = setup_items();
2441 let (rows, _) = query(
2443 &db, "FROM items GROUP BY cat SUM price HAVING sum_price > 20").unwrap();
2444 assert_eq!(rows.len(), 1);
2445 assert_eq!(rows[0]["cat"], "y");
2446 }
2447
2448 #[test]
2451 fn having_supports_the_whole_predicate_surface() {
2452 let (_tmp, db) = setup_items();
2453 let (in_, _) = query(
2454 &db, r#"FROM items GROUP BY cat COUNT HAVING cat IN ("x")"#).unwrap();
2455 assert_eq!(in_.len(), 1);
2456 assert_eq!(in_[0]["cat"], "x");
2457
2458 let (btw, _) = query(
2459 &db, "FROM items GROUP BY cat SUM price HAVING sum_price BETWEEN 10 AND 20").unwrap();
2460 assert_eq!(btw.len(), 1);
2461 assert_eq!(btw[0]["cat"], "x");
2462
2463 let (like, _) = query(
2464 &db, r#"FROM items GROUP BY cat COUNT HAVING cat LIKE "y""#).unwrap();
2465 assert_eq!(like.len(), 1);
2466
2467 let (or_, _) = query(
2468 &db, "FROM items GROUP BY cat SUM price HAVING sum_price < 20 OR count = 3").unwrap();
2469 assert_eq!(or_.len(), 2);
2470 }
2471
2472 #[test]
2475 fn where_and_having_are_different_stages() {
2476 let (_tmp, db) = setup_items();
2477 let (w, _) = query(
2479 &db, "FROM items WHERE price > 10 GROUP BY cat SUM price").unwrap();
2480 let x = w.iter().find(|r| r["cat"] == "x");
2481 assert!(x.is_none(), "x's rows (0,5,10) are all filtered out by WHERE");
2482
2483 let (h, _) = query(
2485 &db, "FROM items GROUP BY cat SUM price HAVING sum_price > 10").unwrap();
2486 assert_eq!(h.len(), 2, "both groups sum above 10 when nothing is pre-filtered");
2487 }
2488
2489 #[test]
2490 fn having_without_an_aggregate_is_an_error() {
2491 let (_tmp, db) = setup();
2492 assert!(query(&db, "FROM blocks HAVING height > 3").is_err());
2496 }
2497
2498 #[test]
2504 fn bare_count_returns_one_row() {
2505 let (_tmp, db) = setup();
2506 let (rows, _) = query(&db, "FROM blocks COUNT").unwrap();
2507 assert_eq!(rows.len(), 1);
2508 assert_eq!(rows[0]["count"], json!(5));
2509 assert_eq!(rows[0]["value"], json!(5));
2510 }
2511
2512 #[test]
2513 fn bare_count_respects_the_filter() {
2514 let (_tmp, db) = setup();
2515 let (rows, _) = query(&db, "FROM blocks WHERE height > 3 COUNT").unwrap();
2516 assert_eq!(rows[0]["count"], json!(2));
2517 }
2518
2519 #[test]
2520 fn bare_sum_avg_min_max() {
2521 let (_tmp, db) = setup();
2522 let (s, _) = query(&db, "FROM blocks SUM n_tx").unwrap();
2524 assert_eq!(s[0]["sum_n_tx"], json!(30), "integer inputs give an integer sum");
2525 let (a, _) = query(&db, "FROM blocks AVG n_tx").unwrap();
2526 assert_eq!(a[0]["avg_n_tx"], json!(6.0));
2527 let (mn, _) = query(&db, "FROM blocks MIN n_tx").unwrap();
2528 assert_eq!(mn[0]["min_n_tx"], json!(2));
2529 let (mx, _) = query(&db, "FROM blocks MAX n_tx").unwrap();
2530 assert_eq!(mx[0]["max_n_tx"], json!(10));
2531 }
2532
2533 #[test]
2541 fn integer_aggregates_stay_integers_and_keep_full_precision() {
2542 let dir = tempdir().unwrap();
2543 let db = Db::open(dir.path(), None).unwrap();
2544 let big: [i64; 3] = [9_007_199_254_740_993, 9_007_199_254_740_995, 1];
2547 for (i, v) in big.iter().enumerate() {
2548 db.put("t", &i.to_string(), serde_json::json!({"v": v}),
2549 vec![], None, None).unwrap();
2550 }
2551 let (s, _) = query(&db, "FROM t SUM v").unwrap();
2552 assert_eq!(s[0]["sum_v"], json!(18_014_398_509_481_989i64),
2553 "exact i64 sum, not a rounded f64");
2554 assert!(s[0]["sum_v"].is_i64(), "must serialise as an integer");
2555
2556 let (mx, _) = query(&db, "FROM t MAX v").unwrap();
2557 assert_eq!(mx[0]["max_v"], json!(9_007_199_254_740_995i64));
2558 let (mn, _) = query(&db, "FROM t MIN v").unwrap();
2559 assert_eq!(mn[0]["min_v"], json!(1));
2560 }
2561
2562 #[test]
2565 fn a_single_float_makes_the_aggregate_fractional() {
2566 let dir = tempdir().unwrap();
2567 let db = Db::open(dir.path(), None).unwrap();
2568 db.put("t", "1", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
2569 db.put("t", "2", serde_json::json!({"v": 2.5}), vec![], None, None).unwrap();
2570 let (s, _) = query(&db, "FROM t SUM v").unwrap();
2571 assert_eq!(s[0]["sum_v"], json!(3.5));
2572 let (a, _) = query(&db, "FROM t AVG v").unwrap();
2574 assert_eq!(a[0]["avg_v"], json!(1.75));
2575 }
2576
2577 #[test]
2580 fn booleans_are_not_aggregated_as_numbers() {
2581 let dir = tempdir().unwrap();
2582 let db = Db::open(dir.path(), None).unwrap();
2583 db.put("t", "1", serde_json::json!({"v": true}), vec![], None, None).unwrap();
2584 db.put("t", "2", serde_json::json!({"v": 5}), vec![], None, None).unwrap();
2585 let (s, _) = query(&db, "FROM t SUM v").unwrap();
2586 assert_eq!(s[0]["sum_v"], json!(5), "the bool contributes nothing");
2587 assert_eq!(s[0]["count"], json!(2), "but it still counts toward the group");
2588 }
2589
2590 #[test]
2593 fn bare_count_of_nothing_is_zero_not_empty() {
2594 let (_tmp, db) = setup();
2595 let (rows, count) = query(&db, "FROM blocks WHERE height > 999 COUNT").unwrap();
2596 assert_eq!(count, 1, "still exactly one row");
2597 assert_eq!(rows[0]["count"], json!(0));
2598
2599 let (g, _) = query(&db, "FROM blocks WHERE height > 999 GROUP BY height COUNT").unwrap();
2601 assert!(g.is_empty());
2602 }
2603
2604 #[test]
2605 fn bare_aggregate_over_an_empty_collection() {
2606 let (_tmp, db) = setup();
2607 let (rows, _) = query(&db, "FROM nonexistent COUNT").unwrap();
2608 assert_eq!(rows[0]["count"], json!(0));
2609 let (s, _) = query(&db, "FROM nonexistent SUM n_tx").unwrap();
2610 assert_eq!(s[0]["sum_n_tx"], Value::Null, "sum of nothing is null, not 0");
2611 }
2612
2613 #[test]
2614 fn bare_aggregate_carries_no_group_key() {
2615 let (_tmp, db) = setup();
2616 let (rows, _) = query(&db, "FROM blocks COUNT").unwrap();
2617 if let Value::Object(m) = &rows[0] {
2618 let mut keys: Vec<&String> = m.keys().collect();
2619 keys.sort();
2620 assert_eq!(keys, vec!["count", "value"]);
2621 } else {
2622 panic!("expected an object");
2623 }
2624 }
2625
2626 #[test]
2633 fn a_field_named_like_a_keyword_is_still_addressable() {
2634 let dir = tempdir().unwrap();
2635 let db = Db::open(dir.path(), None).unwrap();
2636 db.put("t", "1", serde_json::json!({
2637 "count": 5, "min": 1, "max": 9, "sum": 3, "avg": 2,
2638 "value": "keep", "limit": 7, "offset": 8, "group": "g1", "search": "s",
2639 }), vec![], None, None).unwrap();
2640 db.put("t", "2", serde_json::json!({
2641 "count": 1, "min": 0, "max": 2, "sum": 0, "avg": 0,
2642 "value": "drop", "limit": 0, "offset": 0, "group": "g2", "search": "t",
2643 }), vec![], None, None).unwrap();
2644
2645 for (nql, want) in [
2646 ("FROM t WHERE count > 3", "1"),
2647 ("FROM t WHERE min = 1", "1"),
2648 ("FROM t WHERE max >= 9", "1"),
2649 ("FROM t WHERE sum = 3", "1"),
2650 ("FROM t WHERE avg = 2", "1"),
2651 (r#"FROM t WHERE value = "keep""#, "1"),
2652 ("FROM t WHERE limit = 7", "1"),
2653 ("FROM t WHERE offset = 8", "1"),
2654 (r#"FROM t WHERE group = "g1""#, "1"),
2655 ] {
2656 let (rows, _) = query(&db, nql).unwrap();
2657 assert_eq!(rows.len(), 1, "`{}` matched {} rows", nql, rows.len());
2658 assert_eq!(rows[0]["_id"], want, "`{}`", nql);
2659 }
2660
2661 let (ord, _) = query(&db, "FROM t ORDER BY count DESC").unwrap();
2663 assert_eq!(ord[0]["_id"], "1");
2664 let (grp, _) = query(&db, "FROM t GROUP BY group COUNT").unwrap();
2665 assert_eq!(grp.len(), 2);
2666 let keys: Vec<&str> = grp.iter().filter_map(|r| r["group"].as_str()).collect();
2667 assert!(keys.contains(&"g1") && keys.contains(&"g2"), "{:?}", grp);
2668 }
2669
2670 #[test]
2673 fn keyword_matching_stays_case_insensitive() {
2674 let dir = tempdir().unwrap();
2675 let db = Db::open(dir.path(), None).unwrap();
2676 db.put("t", "1", serde_json::json!({"Count": 5, "n": 1}),
2677 vec![], None, None).unwrap();
2678 let (rows, _) = query(&db, "from t where Count = 5 order by n").unwrap();
2680 assert_eq!(rows.len(), 1);
2681 let (miss, _) = query(&db, "FROM t WHERE count = 5").unwrap();
2683 assert!(miss.is_empty(), "`count` and `Count` are distinct field names");
2684 }
2685
2686 #[test]
2687 fn two_aggregates_is_an_error() {
2688 let (_tmp, db) = setup();
2689 assert!(query(&db, "FROM blocks COUNT SUM n_tx").is_err());
2690 assert!(query(&db, "FROM blocks GROUP BY height COUNT SUM n_tx").is_err());
2691 }
2692
2693 #[test]
2694 fn bare_aggregate_with_having() {
2695 let (_tmp, db) = setup();
2696 let (keep, _) = query(&db, "FROM blocks COUNT HAVING count > 3").unwrap();
2697 assert_eq!(keep.len(), 1);
2698 let (drop, _) = query(&db, "FROM blocks COUNT HAVING count > 99").unwrap();
2699 assert!(drop.is_empty());
2700 }
2701
2702 fn setup_items() -> (tempfile::TempDir, Db) {
2707 let dir = tempdir().unwrap();
2708 let db = Db::open(dir.path(), None).unwrap();
2709 for i in 0..6 {
2710 db.put("items", &i.to_string(),
2711 serde_json::json!({"cat": if i < 3 {"x"} else {"y"}, "price": i * 5}),
2712 vec![], None, None).unwrap();
2713 }
2714 (dir, db)
2715 }
2716
2717 fn group(rows: &[Value], field: &str, key: &str) -> Value {
2718 rows.iter()
2719 .find(|r| r[field] == Value::String(key.to_string()))
2720 .unwrap_or_else(|| panic!("no group {:?} in {:?}", key, rows))
2721 .clone()
2722 }
2723
2724 #[test]
2730 fn group_by_aggregates_the_target_field_not_the_group_field() {
2731 let (_tmp, db) = setup_items();
2732
2733 let (mins, _) = query(&db, "FROM items GROUP BY cat MIN price").unwrap();
2734 assert_eq!(group(&mins, "cat", "x")["min_price"], json!(0));
2735 assert_eq!(group(&mins, "cat", "y")["min_price"], json!(15));
2736
2737 let (maxs, _) = query(&db, "FROM items GROUP BY cat MAX price").unwrap();
2738 assert_eq!(group(&maxs, "cat", "y")["max_price"], json!(25));
2739 assert_eq!(group(&maxs, "cat", "x")["max_price"], json!(10));
2740
2741 let (sums, _) = query(&db, "FROM items GROUP BY cat SUM price").unwrap();
2742 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();
2746 assert_eq!(group(&avgs, "cat", "x")["avg_price"], json!(5.0));
2747 assert_eq!(group(&avgs, "cat", "y")["avg_price"], json!(20.0));
2748 }
2749
2750 #[test]
2754 fn group_by_emits_python_parity_keys_and_the_value_alias() {
2755 let (_tmp, db) = setup_items();
2756 let (rows, _) = query(&db, "FROM items GROUP BY cat SUM price").unwrap();
2757 let x = group(&rows, "cat", "x");
2758 assert_eq!(x["sum_price"], json!(15), "python-parity key");
2759 assert_eq!(x["value"], json!(15), "back-compat alias must agree");
2760 assert_eq!(x["count"], json!(3), "count is the group size");
2761 }
2762
2763 #[test]
2767 fn count_is_group_size_while_aggregate_skips_non_numeric() {
2768 let dir = tempdir().unwrap();
2769 let db = Db::open(dir.path(), None).unwrap();
2770 db.put("t", "1", serde_json::json!({"g": "a", "n": 10}), vec![], None, None).unwrap();
2771 db.put("t", "2", serde_json::json!({"g": "a", "n": 20}), vec![], None, None).unwrap();
2772 db.put("t", "3", serde_json::json!({"g": "a", "n": "N/A"}), vec![], None, None).unwrap();
2773
2774 let (rows, _) = query(&db, "FROM t GROUP BY g AVG n").unwrap();
2775 let a = group(&rows, "g", "a");
2776 assert_eq!(a["count"], json!(3), "every row counts toward the group");
2777 assert_eq!(a["avg_n"], json!(15.0), "only the two numeric rows average");
2778 }
2779
2780 #[test]
2782 fn empty_aggregate_input_is_null() {
2783 let dir = tempdir().unwrap();
2784 let db = Db::open(dir.path(), None).unwrap();
2785 db.put("t", "1", serde_json::json!({"g": "a", "n": "x"}), vec![], None, None).unwrap();
2786 let (rows, _) = query(&db, "FROM t GROUP BY g MIN n").unwrap();
2787 assert_eq!(rows[0]["min_n"], Value::Null);
2788 assert_eq!(rows[0]["count"], json!(1));
2789 }
2790
2791 #[test]
2794 fn bare_group_by_without_an_aggregate_counts() {
2795 let (_tmp, db) = setup_items();
2796 let (rows, _) = query(&db, "FROM items GROUP BY cat").unwrap();
2797 assert_eq!(rows.len(), 2);
2798 assert_eq!(group(&rows, "cat", "x")["count"], json!(3));
2799 assert_eq!(group(&rows, "cat", "y")["count"], json!(3));
2800 }
2801
2802 #[test]
2804 fn aggregate_without_a_target_field_is_an_error() {
2805 let (_tmp, db) = setup_items();
2806 for bad in [
2807 "FROM items GROUP BY cat SUM",
2808 "FROM items GROUP BY cat AVG",
2809 "FROM items GROUP BY cat MIN",
2810 ] {
2811 assert!(query(&db, bad).is_err(), "`{}` must be rejected", bad);
2812 }
2813 }
2814
2815 #[test]
2818 fn group_order_is_stable_across_runs() {
2819 let (_tmp, db) = setup_items();
2820 let first = query(&db, "FROM items GROUP BY cat SUM price").unwrap().0;
2821 for _ in 0..8 {
2822 let again = query(&db, "FROM items GROUP BY cat SUM price").unwrap().0;
2823 assert_eq!(first, again);
2824 }
2825 }
2826
2827 #[test]
2829 fn group_by_after_an_in_predicate() {
2830 let (_tmp, db) = setup_items();
2831 let (rows, _) = query(
2832 &db, "FROM items WHERE price IN (0, 5, 25) GROUP BY cat SUM price").unwrap();
2833 assert_eq!(group(&rows, "cat", "x")["sum_price"], json!(5));
2834 assert_eq!(group(&rows, "cat", "y")["sum_price"], json!(25));
2835 }
2836
2837 #[test]
2838 fn search() {
2839 let (_tmp, db) = setup();
2840 let (rows, _) = query(&db, r#"FROM blocks SEARCH "0003""#).unwrap();
2841 assert_eq!(rows.len(), 1);
2842 }
2843
2844 #[test]
2845 fn as_of() {
2846 let dir = tempdir().unwrap();
2847 let db = Db::open(dir.path(), None).unwrap();
2848 let v1 = db.put("docs", "x", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
2849 db.put("docs", "x", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
2850 let (rows, _) = query(&db, &format!("FROM docs AS OF {}", v1.seq)).unwrap();
2851 assert_eq!(rows[0]["v"], 1);
2852 }
2853
2854 #[test]
2855 fn valid_as_of() {
2856 let dir = tempdir().unwrap();
2857 let db = Db::open(dir.path(), None).unwrap();
2858 db.put("events", "e1", serde_json::json!({"type": "a"}), vec![],
2859 Some("2025-01-01".to_string()), Some("2025-06-01".to_string())).unwrap();
2860 db.put("events", "e2", serde_json::json!({"type": "b"}), vec![],
2861 Some("2026-01-01".to_string()), None).unwrap();
2862 let (rows, _) = query(&db, r#"FROM events VALID AS OF "2025-03-01""#).unwrap();
2863 assert_eq!(rows.len(), 1);
2864 assert_eq!(rows[0]["type"], "a");
2865 }
2866
2867 #[test]
2870 fn escaped_quote_matches_a_value_containing_a_quote() {
2871 let dir = tempdir().unwrap();
2872 let db = Db::open(dir.path(), None).unwrap();
2873 db.put("m", "q", serde_json::json!({ "name": "say \"hi\"" }), vec![], None, None)
2874 .unwrap();
2875 db.put("m", "p", serde_json::json!({ "name": "plain" }), vec![], None, None).unwrap();
2876
2877 let (rows, count) = query(&db, r#"FROM m WHERE name = "say \"hi\"""#).unwrap();
2879 assert_eq!(count, 1, "the escaped-quote literal matches exactly one row");
2880 assert_eq!(rows[0]["_id"], "q");
2881 }
2882
2883 #[test]
2884 fn raw_backslash_still_matches_literally() {
2885 let dir = tempdir().unwrap();
2889 let db = Db::open(dir.path(), None).unwrap();
2890 db.put("m", "b", serde_json::json!({ "p": "back\\slash" }), vec![], None, None).unwrap();
2891
2892 let (rows, count) = query(&db, r#"FROM m WHERE p = "back\slash""#).unwrap();
2893 assert_eq!(count, 1, "a raw backslash literal matches as before");
2894 assert_eq!(rows[0]["_id"], "b");
2895 }
2896
2897 #[test]
2898 fn a_quote_can_no_longer_inject_trailing_clauses() {
2899 let dir = tempdir().unwrap();
2904 let db = Db::open(dir.path(), None).unwrap();
2905 db.put("m", "x", serde_json::json!({ "v": "a\"b" }), vec![], None, None).unwrap();
2906 let (rows, count) = query(&db, r#"FROM m WHERE v = "a\"b""#).unwrap();
2907 assert_eq!(count, 1);
2908 assert_eq!(rows[0]["_id"], "x");
2909 }
2910}
2911
2912#[cfg(test)]
2913mod tests_traverse {
2914 use super::*;
2915 use tempfile::tempdir;
2916 use crate::db::Db;
2917
2918 #[test]
2919 fn traverse_one_hop() {
2920 let db = Db::in_memory();
2921 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
2922 db.put("driver", "d2", serde_json::json!({"name": "Carol"}), vec![], None, None).unwrap();
2923 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
2924 db.put("trip", "t2", serde_json::json!({"status": "ok"}), vec![], None, None).unwrap();
2925
2926 db.link("driver:d1", "handles", "trip:t1").unwrap();
2927 db.link("driver:d1", "handles", "trip:t2").unwrap();
2928
2929 let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
2930 assert_eq!(count, 2);
2931 let ids: std::collections::HashSet<&str> = rows.iter()
2932 .filter_map(|r| r["_id"].as_str())
2933 .collect();
2934 assert!(ids.contains("t1") && ids.contains("t2"));
2935 }
2936
2937 #[test]
2938 fn traverse_returns_empty_when_no_links() {
2939 let db = Db::in_memory();
2940 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
2941 let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
2942 assert_eq!(count, 0);
2943 assert!(rows.is_empty());
2944 }
2945
2946 #[test]
2947 fn traverse_multi_source() {
2948 let db = Db::in_memory();
2950 db.put("driver", "d1", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
2951 db.put("driver", "d2", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
2952 db.put("trip", "t1", serde_json::json!({"n": 1}), vec![], None, None).unwrap();
2953 db.put("trip", "t2", serde_json::json!({"n": 2}), vec![], None, None).unwrap();
2954 db.put("trip", "t3", serde_json::json!({"n": 3}), vec![], None, None).unwrap();
2955
2956 db.link("driver:d1", "handles", "trip:t1").unwrap();
2957 db.link("driver:d1", "handles", "trip:t2").unwrap();
2958 db.link("driver:d2", "handles", "trip:t3").unwrap();
2959
2960 let (_rows, count) = query(&db, r#"FROM driver WHERE status = "active" TRAVERSE handles"#).unwrap();
2961 assert_eq!(count, 3);
2962 }
2963
2964 #[test]
2965 fn traverse_nql_keyword_case_insensitive() {
2966 let db = Db::in_memory();
2968 db.put("driver", "d1", serde_json::json!({}), vec![], None, None).unwrap();
2969 db.put("trip", "t1", serde_json::json!({}), vec![], None, None).unwrap();
2970 db.link("driver:d1", "handles", "trip:t1").unwrap();
2971 let (r1, c1) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
2973 assert_eq!(c1, 1);
2974 let (r2, c2) = query(&db, r#"FROM driver WHERE _id = "d1" traverse handles"#).unwrap();
2976 assert_eq!(c2, 1);
2977 assert_eq!(r1[0]["_id"], r2[0]["_id"]);
2978 }
2979
2980 #[test]
2981 fn traverse_durable() {
2982 let dir = tempdir().unwrap();
2983 {
2984 let db = Db::open(dir.path(), None).unwrap();
2985 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
2986 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
2987 db.link("driver:d1", "handles", "trip:t1").unwrap();
2988 }
2989 let db2 = Db::open(dir.path(), None).unwrap();
2990 db2.startup_ready.store(true, std::sync::atomic::Ordering::SeqCst);
2991 let (rows, count) = query(&db2, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
2992 assert_eq!(count, 1);
2993 assert_eq!(rows[0]["_id"], "t1");
2994 }
2995}