Skip to main content

slipql/
parser.rs

1//! Tokens to a [`Query`].
2//!
3//! Keywords are lowercase and case-sensitive. A word that would be a keyword
4//! in another case gets an error saying so, because that is the one mistake
5//! everyone arriving from SQL makes first.
6
7use 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
14/// Parse a query. `from` is required.
15pub fn parse(text: &str) -> Result<Query> {
16    parse_with(text, None)
17}
18
19/// Parse a query, supplying the `from` clause when the text omits one.
20///
21/// This is how a session bound to a directory lets the prompt skip `from`.
22/// A `from` written in the text wins over the default.
23pub 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    /// An error for the token here, naming what was wanted and, when the
101    /// token is a keyword in the wrong case, saying that instead.
102    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    /// The name after `as`: a bare word or a quoted string.
204    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    /// A path: `@path`, or key segments joined by `.` with `[n]` indexes.
241    ///
242    /// A quoted string in path position is a quoted key, as it is in TOML.
243    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            // TOML allows a bare key of digits, which the lexer reads as an
289            // integer. Only a plain decimal one can be a key.
290            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    /// Whether the keyword-shaped word here is followed by something only a
306    /// key could be followed by, which makes it a key after all. `from` and
307    /// `where` are never keys unquoted, since they end the projection list.
308    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    /// A scalar literal in TOML spelling.
428    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}