1use std::path::PathBuf;
8
9use crate::ast::{Op, Path, Predicate, Projection, Query, Segment, Select, Source};
10use crate::error::{Error, Result};
11use crate::lexer::{tokenize, Token, TokenKind};
12use crate::value::Value;
13
14pub fn parse(text: &str) -> Result<Query> {
16 parse_with(text, None)
17}
18
19pub fn parse_with(text: &str, default_source: Option<&Source>) -> Result<Query> {
24 let tokens = tokenize(text)?;
25 let mut parser = Parser { tokens, pos: 0 };
26 let query = parser.query(default_source)?;
27 parser.expect_end()?;
28 Ok(query)
29}
30
31const KEYWORDS: &[&str] = &[
32 "select",
33 "from",
34 "where",
35 "recursive",
36 "as",
37 "and",
38 "or",
39 "not",
40 "exists",
41 "contains",
42 "in",
43 "like",
44 "ilike",
45 "limit",
46 "true",
47 "false",
48 "inf",
49 "nan",
50];
51
52struct Parser {
53 tokens: Vec<Token>,
54 pos: usize,
55}
56
57impl Parser {
58 fn peek(&self) -> &Token {
59 &self.tokens[self.pos]
60 }
61
62 fn peek_kind(&self) -> &TokenKind {
63 &self.peek().kind
64 }
65
66 fn advance(&mut self) -> Token {
67 let token = self.tokens[self.pos].clone();
68 if self.pos + 1 < self.tokens.len() {
69 self.pos += 1;
70 }
71 token
72 }
73
74 fn is_word(&self, word: &str) -> bool {
75 matches!(self.peek_kind(), TokenKind::Word(w) if w == word)
76 }
77
78 fn eat_word(&mut self, word: &str) -> bool {
79 if self.is_word(word) {
80 self.advance();
81 true
82 } else {
83 false
84 }
85 }
86
87 fn eat(&mut self, kind: &TokenKind) -> bool {
88 if self.peek_kind() == kind {
89 self.advance();
90 true
91 } else {
92 false
93 }
94 }
95
96 fn error_here(&self, message: impl Into<String>) -> Error {
97 Error::parse(self.peek().offset, message)
98 }
99
100 fn expected(&self, what: &str) -> Error {
103 if let TokenKind::Word(w) = self.peek_kind() {
104 let lower = w.to_ascii_lowercase();
105 if lower != *w && KEYWORDS.contains(&lower.as_str()) {
106 return self.error_here(format!(
107 "keywords are lowercase: write `{lower}`, not `{w}`"
108 ));
109 }
110 }
111 self.error_here(format!(
112 "expected {what}, found {}",
113 self.peek_kind().describe()
114 ))
115 }
116
117 fn expect_word(&mut self, word: &str) -> Result<()> {
118 if self.eat_word(word) {
119 Ok(())
120 } else {
121 Err(self.expected(&format!("`{word}`")))
122 }
123 }
124
125 fn expect(&mut self, kind: &TokenKind, what: &str) -> Result<()> {
126 if self.eat(kind) {
127 Ok(())
128 } else {
129 Err(self.expected(what))
130 }
131 }
132
133 fn expect_end(&self) -> Result<()> {
134 match self.peek_kind() {
135 TokenKind::End => Ok(()),
136 _ => Err(self.expected("end of query")),
137 }
138 }
139
140 fn query(&mut self, default_source: Option<&Source>) -> Result<Query> {
141 self.expect_word("select")?;
142 let select = self.select_list()?;
143 let from_written = self.is_word("from");
144 let from = if from_written {
145 self.advance();
146 self.source()?
147 } else if let Some(source) = default_source {
148 source.clone()
149 } else {
150 return Err(self.expected("`from`"));
151 };
152 let filter = if self.eat_word("where") {
153 Some(self.predicate()?)
154 } else {
155 None
156 };
157 let limit = if self.eat_word("limit") {
158 Some(self.limit()?)
159 } else {
160 None
161 };
162 if !matches!(self.peek_kind(), TokenKind::End) {
163 let mut clauses = Vec::new();
164 if filter.is_none() && limit.is_none() {
165 if !from_written && default_source.is_some() {
166 clauses.push("`from`");
167 }
168 clauses.push("`where`");
169 }
170 if limit.is_none() {
171 clauses.push("`limit`");
172 }
173 clauses.push("end of query");
174 return Err(self.expected(&clauses.join(", ")));
175 }
176 Ok(Query {
177 select,
178 from,
179 filter,
180 limit,
181 })
182 }
183
184 fn select_list(&mut self) -> Result<Select> {
185 if self.eat(&TokenKind::Star) {
186 return Ok(Select::All);
187 }
188 let mut columns = Vec::new();
189 loop {
190 let path = self.path()?;
191 let alias = if self.eat_word("as") {
192 Some(self.column_name()?)
193 } else {
194 None
195 };
196 columns.push(Projection { path, alias });
197 if !self.eat(&TokenKind::Comma) {
198 return Ok(Select::Columns(columns));
199 }
200 }
201 }
202
203 fn column_name(&mut self) -> Result<String> {
205 match self.peek_kind().clone() {
206 TokenKind::Word(w) => {
207 self.advance();
208 Ok(w)
209 }
210 TokenKind::Str(s) => {
211 self.advance();
212 Ok(s)
213 }
214 _ => Err(self.expected("a column name after `as`")),
215 }
216 }
217
218 fn source(&mut self) -> Result<Source> {
219 let TokenKind::Str(root) = self.peek_kind().clone() else {
220 return Err(self.expected("a quoted directory path after `from`"));
221 };
222 self.advance();
223 let recursive = self.eat_word("recursive");
224 Ok(Source {
225 root: PathBuf::from(root),
226 recursive,
227 })
228 }
229
230 fn limit(&mut self) -> Result<usize> {
231 match *self.peek_kind() {
232 TokenKind::Integer(n) if n >= 0 => {
233 self.advance();
234 usize::try_from(n).map_err(|_| self.error_here("limit is too large"))
235 }
236 _ => Err(self.expected("a non-negative integer after `limit`")),
237 }
238 }
239
240 fn path(&mut self) -> Result<Path> {
244 if let TokenKind::At(name) = self.peek_kind().clone() {
245 if name != "path" {
246 return Err(self.error_here(format!(
247 "unknown built-in column `@{name}`; the only one is `@path`"
248 )));
249 }
250 self.advance();
251 return Ok(Path::ContainerPath);
252 }
253 let mut segments = vec![Segment::Key(self.key_segment()?)];
254 loop {
255 if self.eat(&TokenKind::Dot) {
256 segments.push(Segment::Key(self.key_segment()?));
257 } else if self.eat(&TokenKind::LBracket) {
258 let index = match *self.peek_kind() {
259 TokenKind::Integer(n) if n >= 0 => {
260 self.advance();
261 usize::try_from(n).map_err(|_| self.error_here("index is too large"))?
262 }
263 _ => return Err(self.expected("a non-negative integer index")),
264 };
265 self.expect(&TokenKind::RBracket, "`]`")?;
266 segments.push(Segment::Index(index));
267 } else {
268 return Ok(Path::Keys(segments));
269 }
270 }
271 }
272
273 fn key_segment(&mut self) -> Result<String> {
274 match self.peek_kind().clone() {
275 TokenKind::Word(w) => {
276 if KEYWORDS.contains(&w.as_str()) && !self.word_reads_as_key() {
277 return Err(self.error_here(format!(
278 "`{w}` is a keyword here; quote it to use it as a key"
279 )));
280 }
281 self.advance();
282 Ok(w)
283 }
284 TokenKind::Str(s) => {
285 self.advance();
286 Ok(s)
287 }
288 TokenKind::Integer(n) if n >= 0 => {
291 let token = self.advance();
292 let raw = format!("{n}");
293 if raw.len() != token.len {
294 return Err(Error::parse(
295 token.offset,
296 "a numeric key must be plain decimal digits",
297 ));
298 }
299 Ok(raw)
300 }
301 _ => Err(self.expected("a key")),
302 }
303 }
304
305 fn word_reads_as_key(&self) -> bool {
309 let TokenKind::Word(w) = self.peek_kind() else {
310 return false;
311 };
312 if matches!(
313 w.as_str(),
314 "from" | "where" | "and" | "or" | "not" | "exists" | "limit"
315 ) {
316 return false;
317 }
318 matches!(
319 self.tokens.get(self.pos + 1).map(|t| &t.kind),
320 Some(TokenKind::Dot | TokenKind::LBracket)
321 )
322 }
323
324 fn predicate(&mut self) -> Result<Predicate> {
325 self.disjunction()
326 }
327
328 fn disjunction(&mut self) -> Result<Predicate> {
329 let mut left = self.conjunction()?;
330 while self.eat_word("or") {
331 let right = self.conjunction()?;
332 left = Predicate::Or(Box::new(left), Box::new(right));
333 }
334 Ok(left)
335 }
336
337 fn conjunction(&mut self) -> Result<Predicate> {
338 let mut left = self.negation()?;
339 while self.eat_word("and") {
340 let right = self.negation()?;
341 left = Predicate::And(Box::new(left), Box::new(right));
342 }
343 Ok(left)
344 }
345
346 fn negation(&mut self) -> Result<Predicate> {
347 if self.eat_word("not") {
348 return Ok(Predicate::Not(Box::new(self.negation()?)));
349 }
350 self.atom()
351 }
352
353 fn atom(&mut self) -> Result<Predicate> {
354 if self.eat(&TokenKind::LParen) {
355 let inner = self.predicate()?;
356 self.expect(&TokenKind::RParen, "`)`")?;
357 return Ok(inner);
358 }
359 if self.eat_word("exists") {
360 return Ok(Predicate::Exists(self.path()?));
361 }
362 if matches!(self.peek_kind(), TokenKind::End) {
363 return Err(self.expected("a condition"));
364 }
365 let path = self.path()?;
366 let negated = self.eat_word("not");
367 let test = if let Some(op) = self.comparison_op() {
368 if negated {
369 return Err(self.error_here(
370 "`not` goes before the whole comparison, not before the operator",
371 ));
372 }
373 self.advance();
374 let value = self.scalar_literal("a value to compare against")?;
375 Predicate::Compare { path, op, value }
376 } else if self.eat_word("in") {
377 self.expect(&TokenKind::LParen, "`(` after `in`")?;
378 let mut values = vec![self.scalar_literal("a value")?];
379 while self.eat(&TokenKind::Comma) {
380 values.push(self.scalar_literal("a value")?);
381 }
382 self.expect(&TokenKind::RParen, "`)`")?;
383 Predicate::In { path, values }
384 } else if self.is_word("like") || self.is_word("ilike") {
385 let case_insensitive = self.is_word("ilike");
386 self.advance();
387 let TokenKind::Str(pattern) = self.peek_kind().clone() else {
388 return Err(self.expected("a quoted pattern"));
389 };
390 self.advance();
391 Predicate::Like {
392 path,
393 pattern,
394 case_insensitive,
395 }
396 } else if self.eat_word("contains") {
397 let value = self.scalar_literal("a value after `contains`")?;
398 Predicate::Contains { path, value }
399 } else if self.is_word("exists") {
400 return Err(self.error_here("`exists` goes before the path: `exists tags`"));
401 } else if self.is_word("is") {
402 return Err(self.error_here(
403 "there is no `is null`: TOML has no null, so a key is present or absent; use `exists`",
404 ));
405 } else {
406 return Err(self.expected("a comparison, `in`, `like`, `ilike`, or `contains`"));
407 };
408 Ok(if negated {
409 Predicate::Not(Box::new(test))
410 } else {
411 test
412 })
413 }
414
415 fn comparison_op(&self) -> Option<Op> {
416 Some(match self.peek_kind() {
417 TokenKind::Eq => Op::Eq,
418 TokenKind::Ne => Op::Ne,
419 TokenKind::Lt => Op::Lt,
420 TokenKind::Gt => Op::Gt,
421 TokenKind::Le => Op::Le,
422 TokenKind::Ge => Op::Ge,
423 _ => return None,
424 })
425 }
426
427 fn scalar_literal(&mut self, what: &str) -> Result<Value> {
429 let value = match self.peek_kind().clone() {
430 TokenKind::Str(s) => Value::String(s),
431 TokenKind::Integer(i) => Value::Integer(i),
432 TokenKind::Float(x) => Value::Float(x),
433 TokenKind::Datetime(d) => Value::Datetime(d),
434 TokenKind::Word(w) => match w.as_str() {
435 "true" => Value::Boolean(true),
436 "false" => Value::Boolean(false),
437 "inf" => Value::Float(f64::INFINITY),
438 "nan" => Value::Float(f64::NAN),
439 _ => {
440 return Err(self.error_here(format!(
441 "expected {what}, found `{w}`; a string literal needs quotes",
442 )))
443 }
444 },
445 TokenKind::LBracket => {
446 return Err(
447 self.error_here("array literals are not supported; use `contains` or `in`")
448 )
449 }
450 _ => return Err(self.expected(what)),
451 };
452 self.advance();
453 Ok(value)
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 fn keys(names: &[&str]) -> Path {
462 Path::Keys(
463 names
464 .iter()
465 .map(|n| Segment::Key((*n).to_owned()))
466 .collect(),
467 )
468 }
469
470 #[test]
471 fn full_shape() {
472 let q = parse("select title, owner.name as who from './docs' recursive where status = \"final\" limit 5").unwrap();
473 assert_eq!(
474 q.select,
475 Select::Columns(vec![
476 Projection {
477 path: keys(&["title"]),
478 alias: None
479 },
480 Projection {
481 path: keys(&["owner", "name"]),
482 alias: Some("who".into())
483 },
484 ])
485 );
486 assert_eq!(
487 q.from,
488 Source {
489 root: "./docs".into(),
490 recursive: true
491 }
492 );
493 assert_eq!(
494 q.filter,
495 Some(Predicate::Compare {
496 path: keys(&["status"]),
497 op: Op::Eq,
498 value: Value::String("final".into())
499 })
500 );
501 assert_eq!(q.limit, Some(5));
502 }
503
504 #[test]
505 fn star_and_default_source() {
506 let src = Source {
507 root: ".".into(),
508 recursive: false,
509 };
510 let q = parse_with("select *", Some(&src)).unwrap();
511 assert_eq!(q.select, Select::All);
512 assert_eq!(q.from, src);
513 let q = parse_with("select * from 'x'", Some(&src)).unwrap();
514 assert_eq!(q.from.root, PathBuf::from("x"));
515 assert!(parse("select *").is_err());
516 }
517
518 #[test]
519 fn precedence() {
520 let q = parse("select * from '.' where not a = 1 and b = 2 or c = 3").unwrap();
521 let Some(Predicate::Or(left, right)) = q.filter else {
522 panic!()
523 };
524 assert!(matches!(*right, Predicate::Compare { .. }));
525 let Predicate::And(l, _) = *left else {
526 panic!()
527 };
528 assert!(matches!(*l, Predicate::Not(_)));
529 let q = parse("select * from '.' where a = 1 and (b = 2 or c = 3)").unwrap();
530 assert!(matches!(q.filter, Some(Predicate::And(_, _))));
531 }
532
533 #[test]
534 fn predicate_forms() {
535 let f = |s: &str| {
536 parse(&format!("select * from '.' where {s}"))
537 .unwrap()
538 .filter
539 .unwrap()
540 };
541 assert_eq!(f("exists a.b"), Predicate::Exists(keys(&["a", "b"])));
542 assert_eq!(
543 f("not exists a"),
544 Predicate::Not(Box::new(Predicate::Exists(keys(&["a"]))))
545 );
546 assert_eq!(
547 f("tags contains \"x\""),
548 Predicate::Contains {
549 path: keys(&["tags"]),
550 value: Value::String("x".into())
551 }
552 );
553 assert_eq!(
554 f("n in (1, 2)"),
555 Predicate::In {
556 path: keys(&["n"]),
557 values: vec![Value::Integer(1), Value::Integer(2)]
558 }
559 );
560 assert_eq!(
561 f("n not in (1)"),
562 Predicate::Not(Box::new(Predicate::In {
563 path: keys(&["n"]),
564 values: vec![Value::Integer(1)]
565 }))
566 );
567 assert_eq!(
568 f("t ilike '%a%'"),
569 Predicate::Like {
570 path: keys(&["t"]),
571 pattern: "%a%".into(),
572 case_insensitive: true
573 }
574 );
575 assert!(matches!(f("t not like 'a'"), Predicate::Not(_)));
576 assert_eq!(
577 f("@path like '%.pdf.slpc'"),
578 Predicate::Like {
579 path: Path::ContainerPath,
580 pattern: "%.pdf.slpc".into(),
581 case_insensitive: false
582 }
583 );
584 assert_eq!(
585 f("created >= 2026-01-01"),
586 Predicate::Compare {
587 path: keys(&["created"]),
588 op: Op::Ge,
589 value: Value::Datetime("2026-01-01".parse().unwrap())
590 }
591 );
592 assert_eq!(
593 f("ok = true"),
594 Predicate::Compare {
595 path: keys(&["ok"]),
596 op: Op::Eq,
597 value: Value::Boolean(true)
598 }
599 );
600 }
601
602 #[test]
603 fn paths() {
604 let q = parse("select tags[0], \"my key\".sub, a.\"b.c\" from '.'").unwrap();
605 let Select::Columns(cols) = q.select else {
606 panic!()
607 };
608 assert_eq!(
609 cols[0].path,
610 Path::Keys(vec![Segment::Key("tags".into()), Segment::Index(0)])
611 );
612 assert_eq!(cols[0].column(), "tags[0]");
613 assert_eq!(cols[1].column(), "\"my key\".sub");
614 assert_eq!(cols[2].column(), "a.\"b.c\"");
615 let q = parse("select in.x, 2024 from '.'").unwrap();
616 let Select::Columns(cols) = q.select else {
617 panic!()
618 };
619 assert_eq!(cols[0].column(), "in.x");
620 assert_eq!(cols[1].column(), "2024");
621 }
622
623 #[test]
624 fn helpful_errors() {
625 let msg = |s: &str| parse(s).unwrap_err().to_string();
626 assert!(
627 msg("SELECT * FROM '.'").contains("keywords are lowercase"),
628 "{}",
629 msg("SELECT * FROM '.'")
630 );
631 assert!(msg("select * from '.' where a is null").contains("no `is null`"));
632 assert!(msg("select * from '.' where a = b").contains("needs quotes"));
633 assert!(msg("select @size from '.'").contains("@path"));
634 assert!(msg("select * from '.' where a exists").contains("before the path"));
635 assert!(msg("select * from '.' where in = 1").contains("keyword"));
636 assert!(msg("select * from '.' where a = [1]").contains("array literals"));
637 assert!(msg("select * from '.' where a not = 1").contains("`not` goes before"));
638 assert!(msg("select * from '.' where").contains("condition"));
639 assert!(msg("select * from '.' extra").contains("`where`, `limit`, end of query"));
640 let src = Source {
641 root: ".".into(),
642 recursive: false,
643 };
644 let bound = parse_with("select * frm '.'", Some(&src))
645 .unwrap_err()
646 .to_string();
647 assert!(
648 bound.contains("`from`, `where`, `limit`, end of query"),
649 "{bound}"
650 );
651 let after_where = parse("select * from '.' where a = 1 b")
652 .unwrap_err()
653 .to_string();
654 assert!(
655 after_where.contains("expected `limit`, end of query"),
656 "{after_where}"
657 );
658 assert!(msg("select * from . ").contains("quoted directory"));
659 assert!(msg("select * from '.' limit -1").contains("non-negative"));
660 }
661}