Skip to main content

hayson_engine/query/
parser.rs

1use serde_json::Value;
2
3use crate::errors::HaysonQueryParseError;
4
5use super::planner::{QuerySpec, SortDirection};
6use super::predicates::{Combine, ComparisonOp, PredicateNode};
7
8/// The clauses allowed after the WHERE fragment, in the only order SQL accepts them. The derived
9/// `Ord` is that order, so "came too late" is a plain comparison.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11enum Clause {
12    OrderBy,
13    Limit,
14    Offset,
15}
16
17impl Clause {
18    fn name(self) -> &'static str {
19        match self {
20            Clause::OrderBy => "ORDER BY",
21            Clause::Limit => "LIMIT",
22            Clause::Offset => "OFFSET",
23        }
24    }
25}
26
27#[derive(Debug, Clone, PartialEq)]
28enum Token {
29    Ident(String),
30    String(String),
31    Number(String),
32    Op(&'static str),
33    LParen,
34    RParen,
35    LBracket,
36    RBracket,
37    Comma,
38    Eof,
39}
40
41#[derive(Debug, Clone, PartialEq)]
42struct Spanned {
43    token: Token,
44    position: usize,
45}
46
47fn err(position: usize, reason: impl Into<String>) -> HaysonQueryParseError {
48    HaysonQueryParseError {
49        position,
50        reason: reason.into(),
51    }
52}
53
54fn tokenize(text: &str) -> Result<Vec<Spanned>, HaysonQueryParseError> {
55    let bytes = text.as_bytes();
56    let mut tokens = Vec::new();
57    let mut i = 0usize;
58    while i < bytes.len() {
59        let c = bytes[i] as char;
60        if c.is_whitespace() {
61            i += 1;
62            continue;
63        }
64        let start = i;
65        match c {
66            '(' => {
67                tokens.push(Spanned {
68                    token: Token::LParen,
69                    position: start,
70                });
71                i += 1;
72            }
73            ')' => {
74                tokens.push(Spanned {
75                    token: Token::RParen,
76                    position: start,
77                });
78                i += 1;
79            }
80            '[' => {
81                tokens.push(Spanned {
82                    token: Token::LBracket,
83                    position: start,
84                });
85                i += 1;
86            }
87            ']' => {
88                tokens.push(Spanned {
89                    token: Token::RBracket,
90                    position: start,
91                });
92                i += 1;
93            }
94            ',' => {
95                tokens.push(Spanned {
96                    token: Token::Comma,
97                    position: start,
98                });
99                i += 1;
100            }
101            '=' => {
102                tokens.push(Spanned {
103                    token: Token::Op("="),
104                    position: start,
105                });
106                i += 1;
107            }
108            '!' => {
109                if bytes.get(i + 1) == Some(&b'=') {
110                    tokens.push(Spanned {
111                        token: Token::Op("!="),
112                        position: start,
113                    });
114                    i += 2;
115                } else {
116                    return Err(err(start, "expected \"!=\", found a bare \"!\""));
117                }
118            }
119            '>' => {
120                if bytes.get(i + 1) == Some(&b'=') {
121                    tokens.push(Spanned {
122                        token: Token::Op(">="),
123                        position: start,
124                    });
125                    i += 2;
126                } else {
127                    tokens.push(Spanned {
128                        token: Token::Op(">"),
129                        position: start,
130                    });
131                    i += 1;
132                }
133            }
134            '<' => {
135                if bytes.get(i + 1) == Some(&b'=') {
136                    tokens.push(Spanned {
137                        token: Token::Op("<="),
138                        position: start,
139                    });
140                    i += 2;
141                } else {
142                    tokens.push(Spanned {
143                        token: Token::Op("<"),
144                        position: start,
145                    });
146                    i += 1;
147                }
148            }
149            '\'' | '"' => {
150                let quote = c;
151                let mut value = String::new();
152                i += 1;
153                let mut closed = false;
154                while i < bytes.len() {
155                    let ch = bytes[i] as char;
156                    if ch == quote {
157                        i += 1;
158                        closed = true;
159                        break;
160                    }
161                    if ch == '\\' && i + 1 < bytes.len() {
162                        value.push(bytes[i + 1] as char);
163                        i += 2;
164                        continue;
165                    }
166                    value.push(ch);
167                    i += 1;
168                }
169                if !closed {
170                    return Err(err(start, "unterminated string literal"));
171                }
172                tokens.push(Spanned {
173                    token: Token::String(value),
174                    position: start,
175                });
176            }
177            _ if c.is_ascii_digit() || (c == '-' && bytes.get(i + 1).is_some_and(u8::is_ascii_digit)) => {
178                let mut end = i + 1;
179                while end < bytes.len() && (bytes[end].is_ascii_digit() || bytes[end] == b'.') {
180                    end += 1;
181                }
182                let slice = &text[start..end];
183                tokens.push(Spanned {
184                    token: Token::Number(slice.to_string()),
185                    position: start,
186                });
187                i = end;
188            }
189            _ if c.is_alphabetic() || c == '_' => {
190                let mut end = i + 1;
191                while end < bytes.len() && ((bytes[end] as char).is_alphanumeric() || bytes[end] == b'_') {
192                    end += 1;
193                }
194                tokens.push(Spanned {
195                    token: Token::Ident(text[start..end].to_string()),
196                    position: start,
197                });
198                i = end;
199            }
200            other => return Err(err(start, format!("unexpected character \"{other}\""))),
201        }
202    }
203    let eof_position = bytes.len();
204    tokens.push(Spanned {
205        token: Token::Eof,
206        position: eof_position,
207    });
208    Ok(tokens)
209}
210
211struct Parser<'a> {
212    tokens: &'a [Spanned],
213    pos: usize,
214}
215
216impl<'a> Parser<'a> {
217    fn peek(&self) -> &Spanned {
218        &self.tokens[self.pos]
219    }
220
221    fn advance(&mut self) -> Spanned {
222        let token = self.tokens[self.pos].clone();
223        if self.pos + 1 < self.tokens.len() {
224            self.pos += 1;
225        }
226        token
227    }
228
229    fn expect_ident_keyword(&mut self, keyword: &str) -> bool {
230        if let Token::Ident(name) = &self.peek().token {
231            if name.eq_ignore_ascii_case(keyword) {
232                self.advance();
233                return true;
234            }
235        }
236        false
237    }
238
239    fn peek_at(&self, ahead: usize) -> &Token {
240        let last = self.tokens.len() - 1;
241        &self.tokens[(self.pos + ahead).min(last)].token
242    }
243
244    fn peek_clause(&self) -> Option<Clause> {
245        let Token::Ident(name) = &self.peek().token else {
246            return None;
247        };
248        if name.eq_ignore_ascii_case("order") {
249            Some(Clause::OrderBy)
250        } else if name.eq_ignore_ascii_case("limit") {
251            Some(Clause::Limit)
252        } else if name.eq_ignore_ascii_case("offset") {
253            Some(Clause::Offset)
254        } else {
255            None
256        }
257    }
258
259    /// Whether the query opens with a clause instead of a predicate. Only here is a clause keyword
260    /// ambiguous with a field name (`limit > 5`, `order = 1`), so the next token decides: a
261    /// comparison operator means a predicate, anything else means a clause.
262    fn starts_with_clause(&self) -> bool {
263        match self.peek_clause() {
264            Some(Clause::OrderBy) => {
265                matches!(self.peek_at(1), Token::Ident(next) if next.eq_ignore_ascii_case("by"))
266            }
267            Some(_) => match self.peek_at(1) {
268                Token::Op(_) => false,
269                Token::Ident(next) => !["in", "contains", "startswith"]
270                    .iter()
271                    .any(|word| next.eq_ignore_ascii_case(word)),
272                _ => true,
273            },
274            None => false,
275        }
276    }
277
278    fn parse_clauses(&mut self, mut spec: QuerySpec) -> Result<QuerySpec, HaysonQueryParseError> {
279        let mut previous: Option<Clause> = None;
280        while let Some(clause) = self.peek_clause() {
281            let position = self.peek().position;
282            match previous {
283                Some(seen) if seen == clause => {
284                    return Err(err(
285                        position,
286                        format!("\"{}\" given twice — each clause may appear once", clause.name()),
287                    ))
288                }
289                Some(seen) if clause < seen => {
290                    return Err(err(
291                        position,
292                        format!("\"{}\" must come before \"{}\"", clause.name(), seen.name()),
293                    ))
294                }
295                _ => {}
296            }
297            self.advance();
298            spec = match clause {
299                Clause::OrderBy => {
300                    let by = self.peek().clone();
301                    if !self.expect_ident_keyword("by") {
302                        return Err(err(
303                            by.position,
304                            format!("expected \"BY\" after \"ORDER\", found {}", describe(&by.token)),
305                        ));
306                    }
307                    let field_token = self.peek().clone();
308                    let Token::Ident(field) = field_token.token else {
309                        return Err(err(
310                            field_token.position,
311                            format!(
312                                "expected a field name after \"ORDER BY\", found {}",
313                                describe(&field_token.token)
314                            ),
315                        ));
316                    };
317                    self.advance();
318                    let direction = if self.expect_ident_keyword("desc") {
319                        SortDirection::Desc
320                    } else {
321                        self.expect_ident_keyword("asc");
322                        SortDirection::Asc
323                    };
324                    spec.with_order_by(field, direction)
325                }
326                Clause::Limit => spec.with_limit(self.parse_count(clause)?),
327                Clause::Offset => spec.with_offset(self.parse_count(clause)?),
328            };
329            previous = Some(clause);
330        }
331        Ok(spec)
332    }
333
334    fn parse_count(&mut self, clause: Clause) -> Result<usize, HaysonQueryParseError> {
335        let token = self.peek().clone();
336        let parsed = match &token.token {
337            Token::Number(text) => text.parse::<usize>().ok(),
338            _ => None,
339        };
340        let Some(count) = parsed else {
341            return Err(err(
342                token.position,
343                format!(
344                    "\"{}\" needs a non-negative integer, found {}",
345                    clause.name(),
346                    describe(&token.token)
347                ),
348            ));
349        };
350        self.advance();
351        Ok(count)
352    }
353
354    fn parse_expr(&mut self) -> Result<PredicateNode, HaysonQueryParseError> {
355        let mut terms = vec![self.parse_and()?];
356        while self.expect_ident_keyword("or") {
357            terms.push(self.parse_and()?);
358        }
359        Ok(if terms.len() == 1 {
360            terms.pop().unwrap()
361        } else {
362            PredicateNode::Group {
363                combine: Combine::Or,
364                nodes: terms,
365            }
366        })
367    }
368
369    fn parse_and(&mut self) -> Result<PredicateNode, HaysonQueryParseError> {
370        let mut terms = vec![self.parse_unary()?];
371        while self.expect_ident_keyword("and") {
372            terms.push(self.parse_unary()?);
373        }
374        Ok(if terms.len() == 1 {
375            terms.pop().unwrap()
376        } else {
377            PredicateNode::Group {
378                combine: Combine::And,
379                nodes: terms,
380            }
381        })
382    }
383
384    fn parse_unary(&mut self) -> Result<PredicateNode, HaysonQueryParseError> {
385        if matches!(self.peek().token, Token::LParen) {
386            self.advance();
387            let node = self.parse_expr()?;
388            let closing = self.peek().clone();
389            if !matches!(closing.token, Token::RParen) {
390                return Err(err(
391                    closing.position,
392                    "expected \")\" to close the bracket opened earlier",
393                ));
394            }
395            self.advance();
396            return Ok(node);
397        }
398        self.parse_comparison()
399    }
400
401    fn parse_comparison(&mut self) -> Result<PredicateNode, HaysonQueryParseError> {
402        let field_token = self.peek().clone();
403        let field = match field_token.token {
404            Token::Ident(name) => {
405                self.advance();
406                name
407            }
408            other => {
409                return Err(err(
410                    field_token.position,
411                    format!("expected a field name, found {}", describe(&other)),
412                ))
413            }
414        };
415
416        if self.expect_ident_keyword("in") {
417            let value = self.parse_value()?;
418            if !matches!(value, Value::Array(_)) {
419                return Err(err(
420                    field_token.position,
421                    "\"in\" needs an array of values, e.g. field in [1, 2]",
422                ));
423            }
424            return Ok(PredicateNode::leaf(field, ComparisonOp::In, value));
425        }
426        if self.expect_ident_keyword("contains") {
427            let value = self.parse_value()?;
428            return Ok(PredicateNode::leaf(field, ComparisonOp::Contains, value));
429        }
430        if self.expect_ident_keyword("startswith") {
431            let value = self.parse_value()?;
432            return Ok(PredicateNode::leaf(field, ComparisonOp::StartsWith, value));
433        }
434
435        let op_token = self.peek().clone();
436        let op = match op_token.token {
437            Token::Op("=") => ComparisonOp::Eq,
438            Token::Op("!=") => ComparisonOp::Ne,
439            Token::Op(">") => ComparisonOp::Gt,
440            Token::Op(">=") => ComparisonOp::Gte,
441            Token::Op("<") => ComparisonOp::Lt,
442            Token::Op("<=") => ComparisonOp::Lte,
443            ref other => {
444                return Err(err(
445                    op_token.position,
446                    format!(
447                        "expected a comparison operator (= != > >= < <= in contains startsWith), found {}",
448                        describe(other)
449                    ),
450                ))
451            }
452        };
453        self.advance();
454        let value = self.parse_value()?;
455        Ok(PredicateNode::leaf(field, op, value))
456    }
457
458    fn parse_value(&mut self) -> Result<Value, HaysonQueryParseError> {
459        let token = self.peek().clone();
460        match token.token {
461            Token::String(text) => {
462                self.advance();
463                Ok(Value::String(text))
464            }
465            Token::Number(text) => {
466                self.advance();
467                serde_json::from_str(&text)
468                    .map_err(|_| err(token.position, format!("\"{text}\" is not a valid number")))
469            }
470            Token::Ident(name) if name.eq_ignore_ascii_case("true") => {
471                self.advance();
472                Ok(Value::Bool(true))
473            }
474            Token::Ident(name) if name.eq_ignore_ascii_case("false") => {
475                self.advance();
476                Ok(Value::Bool(false))
477            }
478            Token::Ident(name) if name.eq_ignore_ascii_case("null") => {
479                self.advance();
480                Ok(Value::Null)
481            }
482            Token::LBracket => {
483                self.advance();
484                let mut items = Vec::new();
485                if !matches!(self.peek().token, Token::RBracket) {
486                    items.push(self.parse_value()?);
487                    while matches!(self.peek().token, Token::Comma) {
488                        self.advance();
489                        items.push(self.parse_value()?);
490                    }
491                }
492                let closing = self.peek().clone();
493                if !matches!(closing.token, Token::RBracket) {
494                    return Err(err(closing.position, "expected \"]\" to close the array"));
495                }
496                self.advance();
497                Ok(Value::Array(items))
498            }
499            other => Err(err(
500                token.position,
501                format!(
502                    "expected a value (string, number, true, false, null or array), found {}",
503                    describe(&other)
504                ),
505            )),
506        }
507    }
508}
509
510fn describe(token: &Token) -> String {
511    match token {
512        Token::Ident(name) => format!("identifier \"{name}\""),
513        Token::String(text) => format!("string \"{text}\""),
514        Token::Number(text) => format!("number {text}"),
515        Token::Op(op) => format!("operator \"{op}\""),
516        Token::LParen => "\"(\"".to_string(),
517        Token::RParen => "\")\"".to_string(),
518        Token::LBracket => "\"[\"".to_string(),
519        Token::RBracket => "\"]\"".to_string(),
520        Token::Comma => "\",\"".to_string(),
521        Token::Eof => "end of query".to_string(),
522    }
523}
524
525/// Parses the WHERE-clause fragment of the SQL-like query language from ADR-0009 (cerebro
526/// ADR-6dk7) — a predicate expression combining comparisons with `AND`/`OR` and parentheses —
527/// followed by the optional `ORDER BY <field> [ASC|DESC]`, `LIMIT <n>` and `OFFSET <n>`, in that
528/// order and at most once each. The fragment may be empty when at least one clause follows
529/// (`ORDER BY name LIMIT 5`); an empty text is still an error. `SELECT`/`FROM`/`JOIN`/`MATCH` and
530/// bound parameters are a separate, larger piece and are not covered here — see the query
531/// module's TODO.
532pub fn parse_query(text: &str) -> Result<QuerySpec, HaysonQueryParseError> {
533    let tokens = tokenize(text)?;
534    let mut parser = Parser {
535        tokens: &tokens,
536        pos: 0,
537    };
538    let root = if parser.starts_with_clause() {
539        PredicateNode::Group {
540            combine: Combine::And,
541            nodes: Vec::new(),
542        }
543    } else {
544        match parser.parse_expr()? {
545            PredicateNode::Group {
546                combine: Combine::And,
547                nodes,
548            } => PredicateNode::Group {
549                combine: Combine::And,
550                nodes,
551            },
552            other => PredicateNode::Group {
553                combine: Combine::And,
554                nodes: vec![other],
555            },
556        }
557    };
558    let spec = parser.parse_clauses(QuerySpec::new(root))?;
559    let trailing = parser.peek().clone();
560    if !matches!(trailing.token, Token::Eof) {
561        return Err(err(
562            trailing.position,
563            format!("unexpected {} after a complete expression", describe(&trailing.token)),
564        ));
565    }
566    Ok(spec)
567}
568
569#[cfg(test)]
570mod tests {
571    use serde_json::json;
572
573    use super::*;
574    use crate::query::builder::QueryBuilder;
575
576    #[test]
577    fn parses_a_single_comparison() {
578        let spec = parse_query("age >= 30").unwrap();
579        let expected = QueryBuilder::new().r#where("age", ComparisonOp::Gte, json!(30)).spec();
580        assert_eq!(spec, expected);
581    }
582
583    #[test]
584    fn parses_every_comparison_operator() {
585        let cases: &[(&str, ComparisonOp, Value)] = &[
586            ("role = 'admin'", ComparisonOp::Eq, json!("admin")),
587            ("role != 'admin'", ComparisonOp::Ne, json!("admin")),
588            ("age > 30", ComparisonOp::Gt, json!(30)),
589            ("age >= 30", ComparisonOp::Gte, json!(30)),
590            ("age < 30", ComparisonOp::Lt, json!(30)),
591            ("age <= 30", ComparisonOp::Lte, json!(30)),
592            ("age in [1, 2, 3]", ComparisonOp::In, json!([1, 2, 3])),
593            ("tags contains 'go'", ComparisonOp::Contains, json!("go")),
594            ("name startsWith 'An'", ComparisonOp::StartsWith, json!("An")),
595        ];
596        for (text, op, value) in cases {
597            let spec = parse_query(text).unwrap();
598            let field = text.split_whitespace().next().unwrap();
599            let expected = QueryBuilder::new().r#where(field, *op, value.clone()).spec();
600            assert_eq!(spec, expected, "mismatch for {text:?}");
601        }
602    }
603
604    #[test]
605    fn flat_and_matches_chained_where() {
606        let spec = parse_query("role = 'admin' AND age > 30").unwrap();
607        let expected = QueryBuilder::new()
608            .r#where("role", ComparisonOp::Eq, json!("admin"))
609            .r#where("age", ComparisonOp::Gt, json!(30))
610            .spec();
611        assert_eq!(spec, expected);
612    }
613
614    #[test]
615    fn and_inside_or_bracket_matches_where_any_where_all() {
616        let text = "role = 'admin' OR (role = 'user' AND age > 35)";
617        let spec = parse_query(text).unwrap();
618        let expected = QueryBuilder::new()
619            .where_any(|group| {
620                group
621                    .r#where("role", ComparisonOp::Eq, json!("admin"))
622                    .where_all(|inner| {
623                        inner.r#where("role", ComparisonOp::Eq, json!("user")).r#where(
624                            "age",
625                            ComparisonOp::Gt,
626                            json!(35),
627                        );
628                    });
629            })
630            .spec();
631        assert_eq!(spec, expected);
632    }
633
634    #[test]
635    fn or_bracket_inside_flat_and_matches_where_then_where_any() {
636        let text = "role = 'user' AND (tags contains 'go' OR tags contains 'node')";
637        let spec = parse_query(text).unwrap();
638        let expected = QueryBuilder::new()
639            .r#where("role", ComparisonOp::Eq, json!("user"))
640            .where_any(|group| {
641                group.r#where("tags", ComparisonOp::Contains, json!("go")).r#where(
642                    "tags",
643                    ComparisonOp::Contains,
644                    json!("node"),
645                );
646            })
647            .spec();
648        assert_eq!(spec, expected);
649    }
650
651    #[test]
652    fn rejects_garbage_with_a_positioned_error() {
653        let error = parse_query("age >=").unwrap_err();
654        assert_eq!(error.position, 6);
655
656        let error = parse_query("age 30").unwrap_err();
657        assert!(error.reason.contains("comparison operator"));
658
659        let error = parse_query("age >= 30 age").unwrap_err();
660        assert!(error.reason.contains("unexpected"));
661
662        let error = parse_query("(age >= 30").unwrap_err();
663        assert!(error.reason.contains("\")\""));
664
665        let error = parse_query("age in 30").unwrap_err();
666        assert!(error.reason.contains("array"));
667    }
668
669    #[test]
670    fn error_display_carries_position_and_reason() {
671        let error = parse_query("").unwrap_err();
672        let message = error.to_string();
673        assert!(message.contains("position"));
674        assert!(message.contains(&error.reason));
675    }
676
677    #[test]
678    fn order_by_defaults_to_ascending() {
679        let spec = parse_query("age > 30 ORDER BY age").unwrap();
680        let expected = QueryBuilder::new()
681            .r#where("age", ComparisonOp::Gt, json!(30))
682            .order_by("age", SortDirection::Asc)
683            .spec();
684        assert_eq!(spec, expected);
685
686        let explicit = parse_query("age > 30 ORDER BY age ASC").unwrap();
687        assert_eq!(explicit, expected);
688    }
689
690    #[test]
691    fn order_by_limit_offset_match_the_builder_chain() {
692        let text = "role = 'admin' AND age > 30 ORDER BY created_at DESC LIMIT 10 OFFSET 20";
693        let spec = parse_query(text).unwrap();
694        let expected = QueryBuilder::new()
695            .r#where("role", ComparisonOp::Eq, json!("admin"))
696            .r#where("age", ComparisonOp::Gt, json!(30))
697            .order_by("created_at", SortDirection::Desc)
698            .limit(10)
699            .offset(20)
700            .spec();
701        assert_eq!(spec, expected);
702    }
703
704    #[test]
705    fn clause_keywords_ignore_case_like_and_and_or() {
706        let upper = parse_query("age > 30 ORDER BY age DESC LIMIT 5 OFFSET 2").unwrap();
707        let lower = parse_query("age > 30 order by age desc limit 5 offset 2").unwrap();
708        let mixed = parse_query("age > 30 Order By age Desc Limit 5 Offset 2").unwrap();
709        let expected = QueryBuilder::new()
710            .r#where("age", ComparisonOp::Gt, json!(30))
711            .order_by("age", SortDirection::Desc)
712            .limit(5)
713            .offset(2)
714            .spec();
715        assert_eq!(upper, expected);
716        assert_eq!(lower, expected);
717        assert_eq!(mixed, expected);
718    }
719
720    #[test]
721    fn each_clause_stands_on_its_own() {
722        let limit_zero = parse_query("age > 30 LIMIT 0").unwrap();
723        assert_eq!(
724            limit_zero,
725            QueryBuilder::new()
726                .r#where("age", ComparisonOp::Gt, json!(30))
727                .limit(0)
728                .spec()
729        );
730
731        let offset_only = parse_query("age > 30 OFFSET 3").unwrap();
732        assert_eq!(
733            offset_only,
734            QueryBuilder::new()
735                .r#where("age", ComparisonOp::Gt, json!(30))
736                .offset(3)
737                .spec()
738        );
739
740        let bracket_then_order = parse_query("(role = 'admin' OR role = 'user') ORDER BY role").unwrap();
741        let expected = QueryBuilder::new()
742            .where_any(|group| {
743                group.r#where("role", ComparisonOp::Eq, json!("admin")).r#where(
744                    "role",
745                    ComparisonOp::Eq,
746                    json!("user"),
747                );
748            })
749            .order_by("role", SortDirection::Asc)
750            .spec();
751        assert_eq!(bracket_then_order, expected);
752    }
753
754    #[test]
755    fn clauses_without_a_where_fragment_leave_the_conjunction_empty() {
756        let spec = parse_query("ORDER BY name LIMIT 5").unwrap();
757        let expected = QueryBuilder::new().order_by("name", SortDirection::Asc).limit(5).spec();
758        assert_eq!(spec, expected);
759
760        let limit_only = parse_query("limit 5").unwrap();
761        assert_eq!(limit_only, QueryBuilder::new().limit(5).spec());
762
763        let error = parse_query("").unwrap_err();
764        assert_eq!(error.position, 0);
765        assert!(error.reason.contains("field name"));
766    }
767
768    #[test]
769    fn a_field_named_like_a_clause_keyword_still_works_in_a_predicate() {
770        let spec = parse_query("limit > 5 AND offset in [1, 2] ORDER BY order").unwrap();
771        let expected = QueryBuilder::new()
772            .r#where("limit", ComparisonOp::Gt, json!(5))
773            .r#where("offset", ComparisonOp::In, json!([1, 2]))
774            .order_by("order", SortDirection::Asc)
775            .spec();
776        assert_eq!(spec, expected);
777
778        let leading = parse_query("order = 1 LIMIT 2").unwrap();
779        assert_eq!(
780            leading,
781            QueryBuilder::new()
782                .r#where("order", ComparisonOp::Eq, json!(1))
783                .limit(2)
784                .spec()
785        );
786    }
787
788    #[test]
789    fn rejects_limit_and_offset_that_are_not_non_negative_integers() {
790        let error = parse_query("age > 30 LIMIT -1").unwrap_err();
791        assert_eq!(error.position, 15);
792        assert_eq!(error.reason, "\"LIMIT\" needs a non-negative integer, found number -1");
793
794        let error = parse_query("age > 30 OFFSET 1.5").unwrap_err();
795        assert_eq!(error.position, 16);
796        assert_eq!(
797            error.reason,
798            "\"OFFSET\" needs a non-negative integer, found number 1.5"
799        );
800
801        let error = parse_query("age > 30 LIMIT 'ten'").unwrap_err();
802        assert_eq!(error.position, 15);
803        assert!(error.reason.contains("string \"ten\""));
804
805        let error = parse_query("age > 30 LIMIT").unwrap_err();
806        assert_eq!(error.position, 14);
807        assert!(error.reason.contains("end of query"));
808    }
809
810    #[test]
811    fn rejects_a_clause_given_twice() {
812        let error = parse_query("age > 30 LIMIT 1 LIMIT 2").unwrap_err();
813        assert_eq!(error.position, 17);
814        assert_eq!(error.reason, "\"LIMIT\" given twice — each clause may appear once");
815
816        let error = parse_query("age > 30 ORDER BY age ORDER BY name").unwrap_err();
817        assert_eq!(error.position, 22);
818        assert!(error.reason.contains("\"ORDER BY\" given twice"));
819    }
820
821    #[test]
822    fn rejects_clauses_out_of_sql_order() {
823        let error = parse_query("age > 30 LIMIT 5 ORDER BY age").unwrap_err();
824        assert_eq!(error.position, 17);
825        assert_eq!(error.reason, "\"ORDER BY\" must come before \"LIMIT\"");
826
827        let error = parse_query("age > 30 OFFSET 1 LIMIT 2").unwrap_err();
828        assert_eq!(error.position, 18);
829        assert_eq!(error.reason, "\"LIMIT\" must come before \"OFFSET\"");
830    }
831
832    #[test]
833    fn rejects_a_malformed_order_by() {
834        let error = parse_query("age > 30 ORDER age").unwrap_err();
835        assert_eq!(error.position, 15);
836        assert_eq!(
837            error.reason,
838            "expected \"BY\" after \"ORDER\", found identifier \"age\""
839        );
840
841        let error = parse_query("age > 30 ORDER BY").unwrap_err();
842        assert_eq!(error.position, 17);
843        assert!(error.reason.contains("field name after \"ORDER BY\""));
844
845        let error = parse_query("age > 30 ORDER BY 5").unwrap_err();
846        assert_eq!(error.position, 18);
847        assert!(error.reason.contains("found number 5"));
848
849        let error = parse_query("age > 30 ORDER BY age sideways").unwrap_err();
850        assert_eq!(error.position, 22);
851        assert!(error.reason.contains("unexpected identifier \"sideways\""));
852    }
853}