1use std::collections::HashMap;
15use anyhow::{bail, Result};
16use serde_json::{json, Value};
17
18use crate::db::Db;
19use crate::index::OrderedValue;
20use crate::store::Node;
21
22#[derive(Debug, Clone, PartialEq)]
25enum Tok {
26 Kw(String), Ident(String), Str(String), Num(f64), Op(String), Eof,
32}
33
34struct Lexer<'a> {
35 src: &'a str,
36 pos: usize,
37}
38
39impl<'a> Lexer<'a> {
40 fn new(src: &'a str) -> Self { Self { src, pos: 0 } }
41
42 fn peek_char(&self) -> Option<char> { self.src[self.pos..].chars().next() }
43
44 fn skip_ws(&mut self) {
45 while let Some(c) = self.peek_char() {
46 if c.is_whitespace() { self.pos += c.len_utf8(); } else { break; }
47 }
48 }
49
50 fn next_tok(&mut self) -> Tok {
51 self.skip_ws();
52 if self.pos >= self.src.len() { return Tok::Eof; }
53
54 let c = self.peek_char().unwrap();
55
56 if c == '"' {
68 self.pos += 1;
69 let mut s = String::new();
70 while let Some(ch) = self.peek_char() {
71 if ch == '"' {
72 break;
73 }
74 if ch == '\\' {
75 let next = self.src[self.pos + 1..].chars().next();
79 if next == Some('"') {
80 s.push('"');
81 self.pos += 1 + 1; continue;
83 }
84 }
85 s.push(ch);
86 self.pos += ch.len_utf8();
87 }
88 if self.peek_char() == Some('"') {
89 self.pos += 1;
90 }
91 return Tok::Str(s);
92 }
93
94 if self.pos + 1 < self.src.len() {
96 let two = &self.src[self.pos..self.pos+2];
97 if matches!(two, "!=" | ">=" | "<=") {
98 self.pos += 2;
99 return Tok::Op(two.to_string());
100 }
101 }
102
103 if matches!(c, '=' | '>' | '<') {
105 self.pos += 1;
106 return Tok::Op(c.to_string());
107 }
108
109 if c.is_ascii_digit() || (c == '-' && self.src[self.pos+1..].starts_with(|d: char| d.is_ascii_digit())) {
111 let start = self.pos;
112 if c == '-' { self.pos += 1; }
113 while let Some(d) = self.peek_char() {
114 if d.is_ascii_digit() || d == '.' { self.pos += 1; } else { break; }
115 }
116 let n: f64 = self.src[start..self.pos].parse().unwrap_or(0.0);
117 return Tok::Num(n);
118 }
119
120 if c.is_alphabetic() || c == '_' {
122 let start = self.pos;
123 while let Some(ch) = self.peek_char() {
124 if ch.is_alphanumeric() || ch == '_' || ch == '.' || ch == ':' {
125 self.pos += ch.len_utf8();
126 } else { break; }
127 }
128 let word = &self.src[start..self.pos];
129 let upper = word.to_uppercase();
130 let keywords = ["FROM","AS","OF","VALID","WHERE","AND","ORDER","BY",
131 "DESC","LIMIT","GROUP","COUNT","SUM","AVG","MIN","MAX",
132 "TRACE","TRAVERSE","REVERSE","SEARCH","NOT","NULL","TRUE","FALSE"];
133 if keywords.contains(&upper.as_str()) {
134 return Tok::Kw(upper);
135 }
136 return Tok::Ident(word.to_string());
137 }
138
139 self.pos += c.len_utf8();
141 self.next_tok()
142 }
143
144 fn tokenize(&mut self) -> Vec<Tok> {
145 let mut toks = vec![];
146 loop {
147 let t = self.next_tok();
148 if t == Tok::Eof { break; }
149 toks.push(t);
150 }
151 toks
152 }
153}
154
155#[derive(Debug, Clone)]
158pub struct WhereClause {
159 pub field: String,
160 pub op: String,
161 pub value: Value,
162}
163
164#[derive(Debug, Clone)]
165pub enum GroupAgg { Count, Sum, Avg, Min, Max }
166
167#[derive(Debug, Clone)]
168pub struct Query {
169 pub coll: String,
170 pub as_of: Option<u64>,
171 pub valid_as_of: Option<String>,
172 pub wheres: Vec<WhereClause>,
173 pub search: Option<String>,
174 pub order_by: Option<String>,
175 pub order_desc: bool,
176 pub limit: Option<usize>,
177 pub group_by: Option<(String, GroupAgg)>,
178 pub trace: Option<String>, pub trace_rev: bool,
180 pub traverse: Option<String>, }
182
183struct Parser { toks: Vec<Tok>, pos: usize }
186
187impl Parser {
188 fn new(toks: Vec<Tok>) -> Self { Self { toks, pos: 0 } }
189
190 fn peek(&self) -> &Tok { self.toks.get(self.pos).unwrap_or(&Tok::Eof) }
191 fn advance(&mut self) -> Tok { let t = self.peek().clone(); self.pos += 1; t }
192
193 fn expect_kw(&mut self, kw: &str) -> Result<()> {
194 match self.advance() {
195 Tok::Kw(k) if k == kw => Ok(()),
196 other => bail!("expected keyword {}, got {:?}", kw, other),
197 }
198 }
199
200 fn parse_value(&mut self) -> Value {
201 match self.advance() {
202 Tok::Str(s) => Value::String(s),
203 Tok::Num(n) => json!(n),
204 Tok::Kw(k) if k == "NULL" => Value::Null,
205 Tok::Kw(k) if k == "TRUE" => Value::Bool(true),
206 Tok::Kw(k) if k == "FALSE" => Value::Bool(false),
207 Tok::Ident(s) => Value::String(s),
208 _ => Value::Null,
209 }
210 }
211
212 fn parse(&mut self) -> Result<Query> {
213 self.expect_kw("FROM")?;
214 let coll = match self.advance() {
215 Tok::Ident(s) | Tok::Kw(s) => s,
216 other => bail!("expected collection name, got {:?}", other),
217 };
218
219 let mut q = Query {
220 coll, as_of: None, valid_as_of: None,
221 wheres: vec![], search: None,
222 order_by: None, order_desc: false,
223 limit: None, group_by: None,
224 trace: None, trace_rev: false,
225 traverse: None,
226 };
227
228 loop {
229 match self.peek() {
230 Tok::Eof => break,
231
232 Tok::Kw(k) if k == "AS" => {
233 self.advance();
234 self.expect_kw("OF")?;
235 match self.advance() {
236 Tok::Num(n) => q.as_of = Some(n as u64),
237 other => bail!("AS OF expects sequence number, got {:?}", other),
238 }
239 }
240
241 Tok::Kw(k) if k == "VALID" => {
242 self.advance();
243 self.expect_kw("AS")?;
244 self.expect_kw("OF")?;
245 match self.advance() {
246 Tok::Str(s) => q.valid_as_of = Some(s),
247 other => bail!("VALID AS OF expects date string, got {:?}", other),
248 }
249 }
250
251 Tok::Kw(k) if k == "WHERE" => {
252 self.advance();
253 loop {
254 let field = match self.advance() {
255 Tok::Ident(s) | Tok::Kw(s) => s,
256 other => bail!("WHERE: expected field name, got {:?}", other),
257 };
258 let op = match self.advance() {
259 Tok::Op(s) => s,
260 other => bail!("WHERE: expected operator, got {:?}", other),
261 };
262 let value = self.parse_value();
263 q.wheres.push(WhereClause { field, op, value });
264 if let Tok::Kw(k) = self.peek() {
265 if k == "AND" { self.advance(); } else { break; }
266 } else { break; }
267 }
268 }
269
270 Tok::Kw(k) if k == "SEARCH" => {
271 self.advance();
272 match self.advance() {
273 Tok::Str(s) => q.search = Some(s),
274 other => bail!("SEARCH expects quoted string, got {:?}", other),
275 }
276 }
277
278 Tok::Kw(k) if k == "ORDER" => {
279 self.advance();
280 self.expect_kw("BY")?;
281 let field = match self.advance() {
282 Tok::Ident(s) | Tok::Kw(s) => s,
283 other => bail!("ORDER BY: expected field, got {:?}", other),
284 };
285 q.order_by = Some(field);
286 if let Tok::Kw(k) = self.peek() {
287 if k == "DESC" { self.advance(); q.order_desc = true; }
288 }
289 }
290
291 Tok::Kw(k) if k == "LIMIT" => {
292 self.advance();
293 match self.advance() {
294 Tok::Num(n) => q.limit = Some(n as usize),
295 other => bail!("LIMIT expects number, got {:?}", other),
296 }
297 }
298
299 Tok::Kw(k) if k == "GROUP" => {
300 self.advance();
301 self.expect_kw("BY")?;
302 let field = match self.advance() {
303 Tok::Ident(s) | Tok::Kw(s) => s,
304 other => bail!("GROUP BY: expected field, got {:?}", other),
305 };
306 let agg = match self.advance() {
307 Tok::Kw(a) if a == "COUNT" => GroupAgg::Count,
308 Tok::Kw(a) if a == "SUM" => GroupAgg::Sum,
309 Tok::Kw(a) if a == "AVG" => GroupAgg::Avg,
310 Tok::Kw(a) if a == "MIN" => GroupAgg::Min,
311 Tok::Kw(a) if a == "MAX" => GroupAgg::Max,
312 other => bail!("GROUP BY: expected aggregation, got {:?}", other),
313 };
314 q.group_by = Some((field, agg));
315 }
316
317 Tok::Kw(k) if k == "TRACE" => {
318 self.advance();
319 let edge = match self.advance() {
320 Tok::Ident(s) | Tok::Kw(s) => s,
321 other => bail!("TRACE: expected edge type, got {:?}", other),
322 };
323 q.trace = Some(edge);
324 if let Tok::Kw(k) = self.peek() {
325 if k == "REVERSE" { self.advance(); q.trace_rev = true; }
326 }
327 }
328
329 Tok::Kw(k) if k == "TRAVERSE" => {
330 self.advance();
331 let rel = match self.advance() {
332 Tok::Ident(s) | Tok::Kw(s) => s,
333 other => bail!("TRAVERSE: expected relation name, got {:?}", other),
334 };
335 q.traverse = Some(rel);
336 }
337
338 _ => { self.advance(); } }
340 }
341
342 Ok(q)
343 }
344}
345
346fn matches_where(node: &Node, w: &WhereClause) -> bool {
349 let field_val = if w.field == "_id" {
350 Value::String(node.id.clone())
351 } else if w.field == "_coll" {
352 Value::String(node.coll.clone())
353 } else if w.field == "_hash" {
354 Value::String(node.hash.clone())
355 } else {
356 node.data.get(&w.field).cloned().unwrap_or(Value::Null)
357 };
358
359 let a = OrderedValue::from(&field_val);
360 let b = OrderedValue::from(&w.value);
361
362 match w.op.as_str() {
363 "=" => a == b,
364 "!=" => a != b,
365 ">" => a > b,
366 "<" => a < b,
367 ">=" => a >= b,
368 "<=" => a <= b,
369 _ => false,
370 }
371}
372
373fn matches_valid_as_of(node: &Node, date: &str) -> bool {
374 let from_ok = node.valid_from.as_deref().map(|f| f <= date).unwrap_or(true);
378 let to_ok = node.valid_to.as_deref().map(|t| t > date).unwrap_or(true);
379 from_ok && to_ok
380}
381
382fn node_contains_text(node: &Node, text: &str) -> bool {
383 let s = node.data.to_string().to_lowercase();
384 s.contains(&text.to_lowercase())
385}
386
387fn node_to_json(node: &Node) -> Value {
388 let mut obj = if let Value::Object(m) = &node.data {
389 m.clone()
390 } else {
391 serde_json::Map::new()
392 };
393 obj.insert("_id".to_string(), Value::String(node.id.clone()));
394 obj.insert("_hash".to_string(), Value::String(node.hash.clone()));
395 obj.insert("_seq".to_string(), json!(node.seq));
396 obj.insert("_coll".to_string(), Value::String(node.coll.clone()));
397 if let Some(ref vf) = node.valid_from {
398 obj.insert("_valid_from".to_string(), Value::String(vf.clone()));
399 }
400 if let Some(ref vt) = node.valid_to {
401 obj.insert("_valid_to".to_string(), Value::String(vt.clone()));
402 }
403 if !node.caused_by.is_empty() {
404 obj.insert("_caused_by".to_string(), Value::Array(
405 node.caused_by.iter().map(|h| Value::String(h.clone())).collect()
406 ));
407 }
408 Value::Object(obj)
409}
410
411pub fn parse(nql: &str) -> Result<Query> {
420 let mut lexer = Lexer::new(nql);
421 let toks = lexer.tokenize();
422 let mut parser = Parser::new(toks);
423 parser.parse()
424}
425
426pub fn execute(db: &Db, nql: &str) -> Result<Vec<Value>> {
427 let q = parse(nql)?;
430
431 let id_eq_fast_path: Option<String> = if q.as_of.is_none() && q.trace.is_none() {
438 q.wheres.iter().find_map(|w| {
439 if w.field == "_id" && w.op == "=" {
440 if let Value::String(ref id) = w.value { Some(id.clone()) } else { None }
441 } else { None }
442 })
443 } else { None };
444
445 let candidates: Vec<Node> = if let Some(ref target_id) = id_eq_fast_path {
446 db.get(&q.coll, target_id).into_iter().collect()
448 } else if let Some(seq_target) = q.as_of {
449 db.id_index.list_ids(&q.coll).into_iter()
451 .filter_map(|id| db.get_as_of(&q.coll, &id, seq_target))
452 .collect()
453 } else if let Some(ref order_field) = q.order_by {
454 let has_post_filters = !q.wheres.is_empty()
464 || q.search.is_some()
465 || q.valid_as_of.is_some();
466 let limit = if has_post_filters {
467 9_999_999
468 } else {
469 q.limit.unwrap_or(9_999_999)
470 };
471 if q.order_desc {
472 db.order_by_desc(&q.coll, order_field, limit)
473 } else {
474 db.order_by_asc(&q.coll, order_field, limit)
475 }
476 } else if let (Some(n), true) = (q.limit, q.wheres.is_empty()
477 && q.search.is_none() && q.trace.is_none()
478 && q.traverse.is_none() && q.group_by.is_none()
479 && q.valid_as_of.is_none()) {
480 db.id_index
485 .list_ids(&q.coll)
486 .into_iter()
487 .take(n)
488 .filter_map(|id| db.get(&q.coll, &id))
489 .collect()
490 } else {
491 db.list(&q.coll)
493 };
494
495 let mut rows: Vec<Node> = candidates.into_iter()
498 .filter(|n| q.wheres.iter().all(|w| matches_where(n, w)))
499 .filter(|n| q.valid_as_of.as_deref()
500 .map(|d| matches_valid_as_of(n, d))
501 .unwrap_or(true))
502 .filter(|n| q.search.as_deref()
503 .map(|t| node_contains_text(n, t))
504 .unwrap_or(true))
505 .collect();
506
507 if let Some(ref _edge_type) = q.trace {
510 let limit = q.limit.unwrap_or(1000);
511 let mut traced: Vec<Node> = vec![];
512 for root in &rows {
513 let chain = db.trace(&root.hash, q.trace_rev, limit);
514 traced.extend(chain);
515 }
516 rows = traced;
517 }
518
519 if let Some(ref rel) = q.traverse {
522 let mut traversed: Vec<Node> = vec![];
523 for root in &rows {
524 let frm = format!("{}:{}", root.coll, root.id);
525 let neighbors = db.neighbors(&frm, rel);
526 traversed.extend(neighbors);
527 }
528 rows = traversed;
529 }
530
531 if let Some(ref field) = q.order_by {
534 if q.as_of.is_some() || !q.wheres.is_empty() || q.search.is_some() {
535 rows.sort_by(|a, b| {
537 let av = a.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
538 let bv = b.data.get(field).map(OrderedValue::from).unwrap_or(OrderedValue::Null);
539 if q.order_desc { bv.cmp(&av) } else { av.cmp(&bv) }
540 });
541 }
542 }
543
544 if let Some(n) = q.limit {
547 rows.truncate(n);
548 }
549
550 if let Some((ref group_field, ref agg)) = q.group_by {
553 let mut groups: HashMap<String, Vec<f64>> = HashMap::new();
554 for node in &rows {
555 let key = node.data.get(group_field)
556 .map(|v| v.to_string().trim_matches('"').to_string())
557 .unwrap_or_else(|| "null".to_string());
558 let val = node.data.get(group_field)
559 .and_then(|v| v.as_f64())
560 .unwrap_or(1.0);
561 groups.entry(key).or_default().push(val);
562 }
563 let result: Vec<Value> = groups.into_iter().map(|(k, vals)| {
564 let agg_val = match agg {
565 GroupAgg::Count => vals.len() as f64,
566 GroupAgg::Sum => vals.iter().sum(),
567 GroupAgg::Avg => vals.iter().sum::<f64>() / vals.len() as f64,
568 GroupAgg::Min => vals.iter().cloned().fold(f64::INFINITY, f64::min),
569 GroupAgg::Max => vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
570 };
571 json!({group_field: k, "value": agg_val, "count": vals.len()})
572 }).collect();
573 return Ok(result);
574 }
575
576 Ok(rows.into_iter().map(|n| node_to_json(&n)).collect())
579}
580
581pub fn query(db: &Db, nql: &str) -> Result<(Vec<Value>, usize)> {
583 let rows = execute(db, nql)?;
584 let count = rows.len();
585 Ok((rows, count))
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use tempfile::tempdir;
592 use crate::db::Db;
593
594 fn setup() -> (tempfile::TempDir, Db) {
599 let dir = tempdir().unwrap();
600 let db = Db::open(dir.path(), None).unwrap();
601 db.create_sorted_index("blocks", "height");
602 for h in 1u64..=5 {
603 db.put("blocks", &h.to_string(),
604 serde_json::json!({"height": h, "hash": format!("000{}", h), "n_tx": h * 2}),
605 vec![], None, None).unwrap();
606 }
607 (dir, db)
608 }
609
610 #[test]
611 fn from_all() {
612 let (_tmp, db) = setup();
613 let (rows, count) = query(&db, "FROM blocks").unwrap();
614 assert_eq!(count, 5);
615 let _ = rows;
616 }
617
618 #[test]
619 fn where_eq() {
620 let (_tmp, db) = setup();
621 let (rows, count) = query(&db, r#"FROM blocks WHERE _id = "3""#).unwrap();
622 assert_eq!(count, 1);
623 assert_eq!(rows[0]["_id"], "3");
624 }
625
626 #[test]
627 fn order_by_limit() {
628 let (_tmp, db) = setup();
629 let (rows, count) = query(&db, "FROM blocks ORDER BY height ASC LIMIT 3").unwrap();
630 assert_eq!(count, 3);
631 assert_eq!(rows[0]["height"], 1);
632 assert_eq!(rows[2]["height"], 3);
633 }
634
635 #[test]
636 fn order_by_desc() {
637 let (_tmp, db) = setup();
638 let (rows, _) = query(&db, "FROM blocks ORDER BY height DESC LIMIT 2").unwrap();
639 assert_eq!(rows[0]["height"], 5);
640 }
641
642 #[test]
643 fn where_gt() {
644 let (_tmp, db) = setup();
645 let (rows, _) = query(&db, "FROM blocks WHERE height > 3").unwrap();
646 assert_eq!(rows.len(), 2);
647 }
648
649 #[test]
656 fn where_order_limit_does_not_truncate_before_filter() {
657 let (_tmp, db) = setup();
658 let (rows, count) =
659 query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height LIMIT 2").unwrap();
660 assert_eq!(count, 2, "both matching rows must survive the limit");
661 let heights: Vec<u64> = rows.iter()
662 .filter_map(|r| r["height"].as_u64())
663 .collect();
664 assert_eq!(heights, vec![4, 5]);
665 let (rows_d, _) =
667 query(&db, "FROM blocks WHERE n_tx >= 8 ORDER BY height DESC LIMIT 1").unwrap();
668 assert_eq!(rows_d.len(), 1);
669 assert_eq!(rows_d[0]["height"], 5);
670 }
671
672 #[test]
673 fn group_by_count() {
674 let (_tmp, db) = setup();
675 let (rows, _) = query(&db, "FROM blocks GROUP BY n_tx COUNT").unwrap();
676 assert_eq!(rows.len(), 5); }
678
679 #[test]
680 fn search() {
681 let (_tmp, db) = setup();
682 let (rows, _) = query(&db, r#"FROM blocks SEARCH "0003""#).unwrap();
683 assert_eq!(rows.len(), 1);
684 }
685
686 #[test]
687 fn as_of() {
688 let dir = tempdir().unwrap();
689 let db = Db::open(dir.path(), None).unwrap();
690 let v1 = db.put("docs", "x", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
691 db.put("docs", "x", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
692 let (rows, _) = query(&db, &format!("FROM docs AS OF {}", v1.seq)).unwrap();
693 assert_eq!(rows[0]["v"], 1);
694 }
695
696 #[test]
697 fn valid_as_of() {
698 let dir = tempdir().unwrap();
699 let db = Db::open(dir.path(), None).unwrap();
700 db.put("events", "e1", serde_json::json!({"type": "a"}), vec![],
701 Some("2025-01-01".to_string()), Some("2025-06-01".to_string())).unwrap();
702 db.put("events", "e2", serde_json::json!({"type": "b"}), vec![],
703 Some("2026-01-01".to_string()), None).unwrap();
704 let (rows, _) = query(&db, r#"FROM events VALID AS OF "2025-03-01""#).unwrap();
705 assert_eq!(rows.len(), 1);
706 assert_eq!(rows[0]["type"], "a");
707 }
708
709 #[test]
712 fn escaped_quote_matches_a_value_containing_a_quote() {
713 let dir = tempdir().unwrap();
714 let db = Db::open(dir.path(), None).unwrap();
715 db.put("m", "q", serde_json::json!({ "name": "say \"hi\"" }), vec![], None, None)
716 .unwrap();
717 db.put("m", "p", serde_json::json!({ "name": "plain" }), vec![], None, None).unwrap();
718
719 let (rows, count) = query(&db, r#"FROM m WHERE name = "say \"hi\"""#).unwrap();
721 assert_eq!(count, 1, "the escaped-quote literal matches exactly one row");
722 assert_eq!(rows[0]["_id"], "q");
723 }
724
725 #[test]
726 fn raw_backslash_still_matches_literally() {
727 let dir = tempdir().unwrap();
731 let db = Db::open(dir.path(), None).unwrap();
732 db.put("m", "b", serde_json::json!({ "p": "back\\slash" }), vec![], None, None).unwrap();
733
734 let (rows, count) = query(&db, r#"FROM m WHERE p = "back\slash""#).unwrap();
735 assert_eq!(count, 1, "a raw backslash literal matches as before");
736 assert_eq!(rows[0]["_id"], "b");
737 }
738
739 #[test]
740 fn a_quote_can_no_longer_inject_trailing_clauses() {
741 let dir = tempdir().unwrap();
746 let db = Db::open(dir.path(), None).unwrap();
747 db.put("m", "x", serde_json::json!({ "v": "a\"b" }), vec![], None, None).unwrap();
748 let (rows, count) = query(&db, r#"FROM m WHERE v = "a\"b""#).unwrap();
749 assert_eq!(count, 1);
750 assert_eq!(rows[0]["_id"], "x");
751 }
752}
753
754#[cfg(test)]
755mod tests_traverse {
756 use super::*;
757 use tempfile::tempdir;
758 use crate::db::Db;
759
760 #[test]
761 fn traverse_one_hop() {
762 let db = Db::in_memory();
763 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
764 db.put("driver", "d2", serde_json::json!({"name": "Carol"}), vec![], None, None).unwrap();
765 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
766 db.put("trip", "t2", serde_json::json!({"status": "ok"}), vec![], None, None).unwrap();
767
768 db.link("driver:d1", "handles", "trip:t1").unwrap();
769 db.link("driver:d1", "handles", "trip:t2").unwrap();
770
771 let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
772 assert_eq!(count, 2);
773 let ids: std::collections::HashSet<&str> = rows.iter()
774 .filter_map(|r| r["_id"].as_str())
775 .collect();
776 assert!(ids.contains("t1") && ids.contains("t2"));
777 }
778
779 #[test]
780 fn traverse_returns_empty_when_no_links() {
781 let db = Db::in_memory();
782 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
783 let (rows, count) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
784 assert_eq!(count, 0);
785 assert!(rows.is_empty());
786 }
787
788 #[test]
789 fn traverse_multi_source() {
790 let db = Db::in_memory();
792 db.put("driver", "d1", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
793 db.put("driver", "d2", serde_json::json!({"status": "active"}), vec![], None, None).unwrap();
794 db.put("trip", "t1", serde_json::json!({"n": 1}), vec![], None, None).unwrap();
795 db.put("trip", "t2", serde_json::json!({"n": 2}), vec![], None, None).unwrap();
796 db.put("trip", "t3", serde_json::json!({"n": 3}), vec![], None, None).unwrap();
797
798 db.link("driver:d1", "handles", "trip:t1").unwrap();
799 db.link("driver:d1", "handles", "trip:t2").unwrap();
800 db.link("driver:d2", "handles", "trip:t3").unwrap();
801
802 let (_rows, count) = query(&db, r#"FROM driver WHERE status = "active" TRAVERSE handles"#).unwrap();
803 assert_eq!(count, 3);
804 }
805
806 #[test]
807 fn traverse_nql_keyword_case_insensitive() {
808 let db = Db::in_memory();
810 db.put("driver", "d1", serde_json::json!({}), vec![], None, None).unwrap();
811 db.put("trip", "t1", serde_json::json!({}), vec![], None, None).unwrap();
812 db.link("driver:d1", "handles", "trip:t1").unwrap();
813 let (r1, c1) = query(&db, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
815 assert_eq!(c1, 1);
816 let (r2, c2) = query(&db, r#"FROM driver WHERE _id = "d1" traverse handles"#).unwrap();
818 assert_eq!(c2, 1);
819 assert_eq!(r1[0]["_id"], r2[0]["_id"]);
820 }
821
822 #[test]
823 fn traverse_durable() {
824 let dir = tempdir().unwrap();
825 {
826 let db = Db::open(dir.path(), None).unwrap();
827 db.put("driver", "d1", serde_json::json!({"name": "Bob"}), vec![], None, None).unwrap();
828 db.put("trip", "t1", serde_json::json!({"status": "req"}), vec![], None, None).unwrap();
829 db.link("driver:d1", "handles", "trip:t1").unwrap();
830 }
831 let db2 = Db::open(dir.path(), None).unwrap();
832 db2.startup_ready.store(true, std::sync::atomic::Ordering::SeqCst);
833 let (rows, count) = query(&db2, r#"FROM driver WHERE _id = "d1" TRAVERSE handles"#).unwrap();
834 assert_eq!(count, 1);
835 assert_eq!(rows[0]["_id"], "t1");
836 }
837}