1use serde_json::Value;
2
3use crate::errors::HaysonQueryParseError;
4
5use super::planner::{QuerySpec, SortDirection};
6use super::predicates::{Combine, ComparisonOp, PredicateNode};
7
8#[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 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
525pub 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}