Skip to main content

pylon_core/parse/
mod.rs

1//
2// This source file is part of the Pylon open source project.
3//
4// Copyright (c) 2026 Jaldis B.V.
5//
6// Licensed under the MIT OR Apache-2.0 license (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     https://opensource.org/licenses/MIT
11//     https://www.apache.org/licenses/LICENSE-2.0
12//
13// Unless required by applicable law or agreed to in writing, software
14// distributed under the License is distributed on an "AS IS" BASIS,
15// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16// See the License for the specific language governing permissions and
17// limitations under the License.
18//
19
20pub mod ast;
21mod lexer;
22mod parser;
23
24use crate::error::PyQLSyntaxError;
25
26pub use ast::{Expr, Stmt};
27
28pub fn parse(input: &str) -> Result<Stmt, PyQLSyntaxError> {
29    let tokens = lexer::Lexer::new(input).tokenize()?;
30    parser::Parser::new(tokens).parse_stmt()
31}
32
33/// Parse a PyQL script — one or more statements separated by semicolons.
34pub fn parse_script(input: &str) -> Result<Vec<Stmt>, PyQLSyntaxError> {
35    let tokens = lexer::Lexer::new(input).tokenize()?;
36    parser::Parser::new(tokens).parse_script()
37}
38
39/// Parse a single PyQL expression (used for rewrite handlers, computed-field
40/// bodies, and constraint expressions stored as strings in the schema).
41pub fn parse_expr(input: &str) -> Result<Expr, PyQLSyntaxError> {
42    let tokens = lexer::Lexer::new(input).tokenize()?;
43    parser::Parser::new(tokens).parse_expr()
44}
45
46/// Parse the body of a computed pointer, default, or any other schema-level
47/// PyQL fragment that stands alone rather than sitting inside a statement.
48///
49/// Identical to `parse_expr`, except that trailing `FILTER`/`ORDER BY`/
50/// `OFFSET`/`LIMIT` are allowed without an enclosing `select`: a fragment
51/// has no statement around it to hang them off, so
52/// `".orders order by .created_at desc limit 5"` used to be a bare syntax
53/// error ("expected an expression, found 'order'") and had to be written as
54/// `"(select .orders order by …)"`. Both spellings now parse, to the same
55/// sub-select.
56pub fn parse_pointer_expr(input: &str) -> Result<Expr, PyQLSyntaxError> {
57    let tokens = lexer::Lexer::new(input).tokenize()?;
58    parser::Parser::new(tokens).parse_pointer_expr()
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use ast::*;
65
66    #[test]
67    fn a_module_under_std_takes_its_full_spelling() {
68        let expr = parse_expr("<std::cal::relative_duration>'30 days'").unwrap();
69        let Expr::TypeCast(cast) = expr else { panic!("{expr:?}") };
70        assert_eq!(cast.ty.as_named(), Some((Some("cal"), "relative_duration")));
71    }
72
73    #[test]
74    fn an_insert_needs_no_shape() {
75        let Stmt::Insert(ins) = parse("insert Person").unwrap() else {
76            panic!()
77        };
78        assert!(ins.shape.is_empty());
79    }
80
81    #[test]
82    fn test_select_bare_type() {
83        let stmt = parse("SELECT Person").unwrap();
84        assert!(matches!(
85            stmt,
86            Stmt::Select(SelectStmt {
87                result: Expr::Path(_),
88                ..
89            })
90        ));
91    }
92
93    #[test]
94    fn test_select_with_shape() {
95        let stmt = parse("SELECT Person { name, age }").unwrap();
96        let Stmt::Select(sel) = stmt else {
97            panic!("not a select")
98        };
99        let Expr::Shape(shape) = sel.result else {
100            panic!("not a shape")
101        };
102        assert_eq!(shape.elements.len(), 2);
103        assert_eq!(shape.elements[0].path, Path::relative("name"));
104        assert_eq!(shape.elements[1].path, Path::relative("age"));
105    }
106
107    #[test]
108    fn test_select_filter_param() {
109        let stmt = parse("SELECT Person { name, age } FILTER .name = $name").unwrap();
110        let Stmt::Select(sel) = stmt else { panic!() };
111        assert!(sel.filter.is_some());
112        let filter = sel.filter.unwrap();
113        let Expr::BinOp(binop) = filter else {
114            panic!("not a binop")
115        };
116        assert_eq!(binop.op, BinOpKind::Eq);
117        assert!(matches!(binop.left, Expr::Path(Path { partial: true, .. })));
118        assert!(matches!(binop.right, Expr::Parameter(_)));
119    }
120
121    #[test]
122    fn test_shape_element_bare_select_without_parens() {
123        let stmt = parse("SELECT Person { primary := SELECT .emails FILTER .primary = true LIMIT 1 }").unwrap();
124        let Stmt::Select(sel) = stmt else { panic!() };
125        let Expr::Shape(shape) = sel.result else {
126            panic!("not a shape")
127        };
128        assert_eq!(shape.elements.len(), 1);
129        let Some(Expr::SubQuery(inner)) = &shape.elements[0].compexpr else {
130            panic!("not a subquery")
131        };
132        let Stmt::Select(inner) = inner.as_ref() else { panic!() };
133        assert!(inner.filter.is_some());
134        assert!(inner.limit.is_some());
135    }
136
137    #[test]
138    fn test_bare_select_stops_at_shape_separator() {
139        let stmt = parse("SELECT Person { a := SELECT .emails LIMIT 1, b := .name }").unwrap();
140        let Stmt::Select(sel) = stmt else { panic!() };
141        let Expr::Shape(shape) = sel.result else {
142            panic!("not a shape")
143        };
144        assert_eq!(shape.elements.len(), 2);
145        assert!(matches!(shape.elements[1].compexpr, Some(Expr::Path(_))));
146    }
147
148    #[test]
149    fn test_bare_with_in_expression_position() {
150        let expr = parse_expr("WITH n := 1 SELECT n").unwrap();
151        assert!(matches!(expr, Expr::SubQuery(_)));
152    }
153
154    #[test]
155    fn test_bare_for_union_in_expression_position() {
156        let expr = parse_expr("FOR x IN {1, 2} UNION (x + 1)").unwrap();
157        assert!(matches!(expr, Expr::SubQuery(_)));
158    }
159
160    #[test]
161    fn test_cast_may_declare_a_parameter_cardinality() {
162        for query in ["<optional std::str>$token", "<required std::str>$token"] {
163            let expr = parse_expr(query).unwrap_or_else(|e| panic!("{query}: {e}"));
164            assert!(matches!(expr, Expr::TypeCast(_)), "{query}");
165        }
166        // Still a comparison, not a cast.
167        let expr = parse_expr("1 < 2").unwrap();
168        assert!(matches!(expr, Expr::BinOp(_)));
169    }
170
171    #[test]
172    fn test_keyword_used_as_a_name_keeps_its_written_casing() {
173        let expr = parse_expr(".<order[is OrderAttribute]").unwrap();
174        let Expr::Path(path) = expr else { panic!("not a path") };
175        assert_eq!(path.steps[0], PathStep::Backlink("order".into()));
176        let expr = parse_expr(".<Order[is OrderAttribute]").unwrap();
177        let Expr::Path(path) = expr else { panic!("not a path") };
178        assert_eq!(path.steps[0], PathStep::Backlink("Order".into()));
179    }
180
181    #[test]
182    fn test_string_literal_preserves_multibyte_utf8() {
183        // Regression: the lexer scans raw bytes; a naive `byte as char` cast
184        // on non-ASCII bytes previously corrupted multi-byte UTF-8 sequences
185        // (each byte became its own Latin-1-style codepoint instead of being
186        // reassembled into the real scalar value).
187        let expr = parse_expr("'I ❤️ Pylon!'").unwrap();
188        assert!(matches!(expr, Expr::Literal(Literal::Str(s)) if s == "I ❤️ Pylon!"));
189    }
190
191    #[test]
192    fn test_positional_param_parsed() {
193        let expr = parse_expr("$0").unwrap();
194        assert!(matches!(expr, Expr::Parameter(n) if n == "0"));
195    }
196
197    #[test]
198    fn test_multiple_positional_params_parsed() {
199        let stmt = parse("SELECT Person FILTER .name = $0 AND .age > $1").unwrap();
200        let Stmt::Select(sel) = stmt else { panic!() };
201        let filter = sel.filter.unwrap();
202        let Expr::BinOp(outer) = filter else {
203            panic!("not a binop")
204        };
205        let Expr::BinOp(left) = outer.left else {
206            panic!("left not binop")
207        };
208        assert!(matches!(left.right, Expr::Parameter(n) if n == "0"));
209        let Expr::BinOp(right) = outer.right else {
210            panic!("right not binop")
211        };
212        assert!(matches!(right.right, Expr::Parameter(n) if n == "1"));
213    }
214
215    #[test]
216    fn test_select_nested_shape() {
217        let stmt = parse("SELECT Person { name, posts { title, body } }").unwrap();
218        let Stmt::Select(sel) = stmt else { panic!() };
219        let Expr::Shape(shape) = sel.result else { panic!() };
220        assert_eq!(shape.elements.len(), 2);
221        let posts = &shape.elements[1];
222        assert_eq!(posts.path, Path::relative("posts"));
223        assert!(posts.nested.is_some());
224        assert_eq!(posts.nested.as_ref().unwrap().len(), 2);
225    }
226
227    #[test]
228    fn test_select_set_literal() {
229        let stmt = parse("SELECT {1, 2, 3}").unwrap();
230        let Stmt::Select(sel) = stmt else { panic!() };
231        let Expr::Set(elems) = sel.result else {
232            panic!("expected Set")
233        };
234        assert_eq!(elems.len(), 3);
235        assert!(matches!(elems[0], Expr::Literal(Literal::Int(1))));
236    }
237
238    #[test]
239    fn test_select_set_literal_single() {
240        let stmt = parse("SELECT {42}").unwrap();
241        let Stmt::Select(sel) = stmt else { panic!() };
242        let Expr::Set(elems) = sel.result else {
243            panic!("expected Set")
244        };
245        assert_eq!(elems.len(), 1);
246    }
247
248    #[test]
249    fn test_analyze_wraps_inner_stmt() {
250        let stmt = parse("analyze select Person { name }").unwrap();
251        let Stmt::Analyze(inner) = stmt else {
252            panic!("expected Analyze")
253        };
254        assert!(matches!(*inner, Stmt::Select(_)));
255    }
256
257    #[test]
258    fn test_analyze_is_case_insensitive_and_not_reserved_elsewhere() {
259        assert!(matches!(parse("ANALYZE select Person").unwrap(), Stmt::Analyze(_)));
260        // "analyze" stays a legal identifier everywhere except as the
261        // leading token of a statement — it's a soft keyword, not reserved.
262        let stmt = parse("SELECT Person { name }").unwrap();
263        assert!(matches!(stmt, Stmt::Select(_)));
264    }
265
266    #[test]
267    fn test_analyze_wraps_insert_and_update_too() {
268        assert!(matches!(
269            parse("analyze insert Person { name := 'a' }").unwrap(),
270            Stmt::Analyze(inner) if matches!(*inner, Stmt::Insert(_))
271        ));
272        assert!(matches!(
273            parse("analyze update Person set { name := 'a' }").unwrap(),
274            Stmt::Analyze(inner) if matches!(*inner, Stmt::Update(_))
275        ));
276    }
277
278    #[test]
279    fn test_analyze_marker_offsets_mark_root_and_nested_shape_elements() {
280        let query = "analyze select Person { name, posts { title } }";
281        let stmt = parse(query).unwrap();
282        let Stmt::Analyze(inner) = stmt else {
283            panic!("expected Analyze")
284        };
285        let Stmt::Select(sel) = *inner else { panic!() };
286        let Expr::Shape(shape) = sel.result else { panic!() };
287
288        // Root marker sits right at "Person", after "analyze select ".
289        let root_offset = shape.marker_offset.expect("root shape should carry an offset");
290        assert_eq!(&query[root_offset..root_offset + "Person".len()], "Person");
291
292        let name_offset = shape.elements[0]
293            .marker_offset
294            .expect("name element should carry an offset");
295        assert_eq!(&query[name_offset..name_offset + "name".len()], "name");
296
297        let posts = &shape.elements[1];
298        let posts_offset = posts.marker_offset.expect("posts element should carry an offset");
299        assert_eq!(&query[posts_offset..posts_offset + "posts".len()], "posts");
300
301        let title_offset = posts.nested.as_ref().unwrap()[0]
302            .marker_offset
303            .expect("nested element should carry an offset");
304        assert_eq!(&query[title_offset..title_offset + "title".len()], "title");
305    }
306
307    #[test]
308    fn test_select_free_object() {
309        let stmt = parse("SELECT { foo := 'bar', n := 42 }").unwrap();
310        let Stmt::Select(sel) = stmt else { panic!() };
311        let Expr::Shape(sh) = sel.result else {
312            panic!("expected Shape")
313        };
314        assert!(sh.expr.is_none());
315        assert_eq!(sh.elements.len(), 2);
316        assert_eq!(sh.elements[0].path, Path::relative("foo"));
317        assert!(sh.elements[0].compexpr.is_some());
318    }
319
320    #[test]
321    fn test_select_tuple_expr() {
322        let stmt = parse("SELECT (1, 'hello')").unwrap();
323        let Stmt::Select(sel) = stmt else { panic!() };
324        assert!(matches!(sel.result, Expr::Tuple(_)));
325    }
326
327    #[test]
328    fn test_select_scalar_function() {
329        let stmt = parse("SELECT str_lower('HELLO')").unwrap();
330        let Stmt::Select(sel) = stmt else { panic!() };
331        assert!(matches!(sel.result, Expr::FunctionCall(_)));
332    }
333
334    #[test]
335    fn test_shape_splat_shallow() {
336        let stmt = parse("SELECT Person { * }").unwrap();
337        let Stmt::Select(sel) = stmt else { panic!() };
338        let Expr::Shape(sh) = &sel.result else { panic!() };
339        assert_eq!(sh.elements.len(), 1);
340        assert!(matches!(sh.elements[0].splat, Some(ast::Splat::Shallow)));
341    }
342
343    #[test]
344    fn test_shape_splat_deep() {
345        let stmt = parse("SELECT Person { ** }").unwrap();
346        let Stmt::Select(sel) = stmt else { panic!() };
347        let Expr::Shape(sh) = &sel.result else { panic!() };
348        assert_eq!(sh.elements.len(), 1);
349        assert!(matches!(sh.elements[0].splat, Some(ast::Splat::Deep)));
350    }
351
352    #[test]
353    fn test_select_order_by_limit_offset() {
354        let stmt = parse("SELECT Person { name } ORDER BY .name ASC OFFSET 10 LIMIT 5").unwrap();
355        let Stmt::Select(sel) = stmt else { panic!() };
356        assert_eq!(sel.order_by.len(), 1);
357        assert_eq!(sel.order_by[0].direction, SortDirection::Asc);
358        assert!(matches!(sel.offset, Some(Expr::Literal(Literal::Int(10)))));
359        assert!(matches!(sel.limit, Some(Expr::Literal(Literal::Int(5)))));
360    }
361
362    #[test]
363    fn test_select_for_update_defaults_to_blocking() {
364        let stmt = parse("SELECT Person FOR UPDATE").unwrap();
365        let Stmt::Select(sel) = stmt else { panic!() };
366        assert_eq!(
367            sel.lock,
368            Some(LockClause {
369                strength: LockStrength::Update,
370                wait: LockWait::Block
371            })
372        );
373    }
374
375    #[test]
376    fn test_select_for_update_skip_locked() {
377        let stmt = parse("SELECT Person FOR UPDATE SKIP LOCKED").unwrap();
378        let Stmt::Select(sel) = stmt else { panic!() };
379        assert_eq!(
380            sel.lock,
381            Some(LockClause {
382                strength: LockStrength::Update,
383                wait: LockWait::SkipLocked
384            })
385        );
386    }
387
388    #[test]
389    fn test_select_for_update_nowait() {
390        let stmt = parse("SELECT Person FOR UPDATE NOWAIT").unwrap();
391        let Stmt::Select(sel) = stmt else { panic!() };
392        assert_eq!(
393            sel.lock,
394            Some(LockClause {
395                strength: LockStrength::Update,
396                wait: LockWait::NoWait
397            })
398        );
399    }
400
401    #[test]
402    fn test_select_for_share_skip_locked_is_case_insensitive() {
403        let stmt = parse("select Person for share skip locked").unwrap();
404        let Stmt::Select(sel) = stmt else { panic!() };
405        assert_eq!(
406            sel.lock,
407            Some(LockClause {
408                strength: LockStrength::Share,
409                wait: LockWait::SkipLocked
410            })
411        );
412    }
413
414    #[test]
415    fn test_select_for_no_key_update() {
416        let stmt = parse("SELECT Person FOR NO KEY UPDATE").unwrap();
417        let Stmt::Select(sel) = stmt else { panic!() };
418        assert_eq!(
419            sel.lock,
420            Some(LockClause {
421                strength: LockStrength::NoKeyUpdate,
422                wait: LockWait::Block
423            })
424        );
425    }
426
427    #[test]
428    fn test_select_for_key_share() {
429        let stmt = parse("SELECT Person FOR KEY SHARE").unwrap();
430        let Stmt::Select(sel) = stmt else { panic!() };
431        assert_eq!(
432            sel.lock,
433            Some(LockClause {
434                strength: LockStrength::KeyShare,
435                wait: LockWait::Block
436            })
437        );
438    }
439
440    #[test]
441    fn test_select_for_update_comes_after_order_by_limit_offset() {
442        let stmt = parse("SELECT Person { name } ORDER BY .name OFFSET 1 LIMIT 5 FOR UPDATE SKIP LOCKED").unwrap();
443        let Stmt::Select(sel) = stmt else { panic!() };
444        assert_eq!(sel.order_by.len(), 1);
445        assert!(sel.offset.is_some());
446        assert!(sel.limit.is_some());
447        assert_eq!(
448            sel.lock,
449            Some(LockClause {
450                strength: LockStrength::Update,
451                wait: LockWait::SkipLocked
452            })
453        );
454    }
455
456    #[test]
457    fn test_select_with_no_lock_clause_defaults_to_none() {
458        let stmt = parse("SELECT Person").unwrap();
459        let Stmt::Select(sel) = stmt else { panic!() };
460        assert_eq!(sel.lock, None);
461    }
462
463    #[test]
464    fn test_select_for_garbage_strength_is_a_clear_error() {
465        let err = parse("SELECT Person FOR BOGUS").unwrap_err();
466        assert!(err.message.contains("UPDATE"), "unexpected: {}", err.message);
467    }
468
469    #[test]
470    fn test_select_for_no_without_key_is_a_clear_error() {
471        let err = parse("SELECT Person FOR NO UPDATE").unwrap_err();
472        assert!(err.message.contains("KEY"), "unexpected: {}", err.message);
473    }
474
475    #[test]
476    fn test_select_for_skip_without_locked_is_a_clear_error() {
477        let err = parse("SELECT Person FOR UPDATE SKIP").unwrap_err();
478        assert!(err.message.contains("LOCKED"), "unexpected: {}", err.message);
479    }
480
481    #[test]
482    fn test_boolean_operators() {
483        let stmt = parse("SELECT Person FILTER .active = true AND .age >= 18 OR .admin = true").unwrap();
484        assert!(matches!(stmt, Stmt::Select(_)));
485    }
486
487    #[test]
488    fn test_function_call() {
489        let stmt = parse("SELECT count(Person)").unwrap();
490        let Stmt::Select(sel) = stmt else { panic!() };
491        assert!(matches!(sel.result, Expr::FunctionCall(_)));
492    }
493
494    #[test]
495    fn test_type_cast() {
496        let stmt = parse("SELECT <str>$value").unwrap();
497        let Stmt::Select(sel) = stmt else { panic!() };
498        assert!(matches!(sel.result, Expr::TypeCast(_)));
499    }
500
501    #[test]
502    fn test_computed_shape_element() {
503        let stmt = parse("SELECT Person { full_name := .first ++ ' ' ++ .last }").unwrap();
504        let Stmt::Select(sel) = stmt else { panic!() };
505        let Expr::Shape(shape) = sel.result else { panic!() };
506        let el = &shape.elements[0];
507        assert!(el.compexpr.is_some());
508    }
509
510    #[test]
511    fn test_insert() {
512        let stmt = parse("INSERT Person { name := 'Alice', age := 30 }").unwrap();
513        let Stmt::Insert(ins) = stmt else { panic!() };
514        assert_eq!(ins.subject.name, "Person");
515        assert_eq!(ins.shape.len(), 2);
516    }
517
518    #[test]
519    fn test_update() {
520        let stmt = parse("UPDATE Person FILTER .name = 'Alice' SET { age := 31 }").unwrap();
521        assert!(matches!(stmt, Stmt::Update(_)));
522    }
523
524    #[test]
525    fn test_delete() {
526        let stmt = parse("DELETE Person FILTER .name = 'Alice'").unwrap();
527        assert!(matches!(stmt, Stmt::Delete(_)));
528    }
529
530    #[test]
531    fn test_if_else_postfix() {
532        let stmt = parse("SELECT 'yes' IF 1 = 1 ELSE 'no'").unwrap();
533        let Stmt::Select(sel) = stmt else { panic!() };
534        let Expr::IfElse(ie) = sel.result else {
535            panic!("expected IfElse")
536        };
537        assert!(matches!(ie.if_expr, Expr::Literal(Literal::Str(_))));
538        assert!(matches!(ie.condition, Expr::BinOp(_)));
539        assert!(matches!(ie.else_expr, Expr::Literal(Literal::Str(_))));
540    }
541
542    #[test]
543    fn test_if_then_else_prefix() {
544        let stmt = parse("SELECT IF 1 = 1 THEN 'yes' ELSE 'no'").unwrap();
545        let Stmt::Select(sel) = stmt else { panic!() };
546        let Expr::IfElse(ie) = sel.result else {
547            panic!("expected IfElse")
548        };
549        assert!(matches!(ie.if_expr, Expr::Literal(Literal::Str(_))));
550        assert!(matches!(ie.condition, Expr::BinOp(_)));
551        assert!(matches!(ie.else_expr, Expr::Literal(Literal::Str(_))));
552    }
553
554    #[test]
555    fn test_if_then_else_chained() {
556        let stmt = parse("SELECT IF 1 = 1 THEN 'a' ELSE IF 2 = 2 THEN 'b' ELSE 'c'").unwrap();
557        let Stmt::Select(sel) = stmt else { panic!() };
558        let Expr::IfElse(outer) = sel.result else {
559            panic!("expected IfElse")
560        };
561        assert!(matches!(outer.else_expr, Expr::IfElse(_)));
562    }
563
564    #[test]
565    fn test_nested_module_path_in_function_call_is_a_clear_error() {
566        // Regression: `ext::pgcrypto::digest(...)` (a 3-segment module path
567        // — PyQL module names are a single segment, e.g. `crypto::digest`)
568        // used to silently stop consuming after the first `::`, leaving the
569        // second `::` dangling to surface as a confusing "unexpected token
570        // ColonColon" once the (wrongly 2-segment-terminated) call returned.
571        let err = parse("select ext::pgcrypto::digest('encrypt this', 'sha1')").unwrap_err();
572        assert!(err.to_string().contains("too many '::' segments"), "got: {err}");
573    }
574
575    #[test]
576    fn test_nested_module_path_in_type_expr_is_a_clear_error() {
577        let err = parse_expr("x is ext::pgcrypto::SomeType").unwrap_err();
578        assert!(err.to_string().contains("too many '::' segments"), "got: {err}");
579    }
580
581    // ── Error message wording ───────────────────────────────────────────────
582    //
583    // Regression: `eat`/`eat_ident` used to Debug-format the `Token` enum
584    // directly (`"expected RParen, got Eof"`, `"unexpected token LBrace"`),
585    // leaking internal lexer variant names instead of the surface syntax a
586    // user actually typed.
587
588    #[test]
589    fn test_unclosed_paren_names_the_missing_character_not_the_token_variant() {
590        let err = parse("select (1 + 2").unwrap_err();
591        assert_eq!(err.message, "expected ')', found end of input");
592    }
593
594    #[test]
595    fn test_unclosed_brace_names_the_missing_character_not_the_token_variant() {
596        let err = parse("select { 1").unwrap_err();
597        assert_eq!(err.message, "expected '}', found end of input");
598    }
599
600    #[test]
601    fn test_trailing_garbage_after_a_complete_statement_is_a_clear_error() {
602        let err = parse("select 1 select 2").unwrap_err();
603        assert_eq!(err.message, "unexpected 'select' after the end of the query");
604    }
605
606    #[test]
607    fn a_script_parses_every_statement() {
608        let statements = parse_script("delete Person; delete Company; select 1;").expect("a script parses");
609        assert_eq!(statements.len(), 3);
610    }
611
612    #[test]
613    fn a_single_statement_is_a_one_element_script() {
614        assert_eq!(parse_script("select 1").expect("parses").len(), 1);
615        assert_eq!(parse_script("select 1;").expect("parses").len(), 1);
616    }
617
618    #[test]
619    fn a_script_still_rejects_trailing_rubbish() {
620        assert!(parse_script("select 1; )").is_err());
621    }
622
623    #[test]
624    fn test_missing_expression_names_the_offending_token_not_its_debug_form() {
625        let err = parse_expr("1 +").unwrap_err();
626        assert_eq!(err.message, "expected an expression, found end of input");
627    }
628
629    #[test]
630    fn test_bad_statement_start_lists_keywords_in_surface_form() {
631        let err = parse("123").unwrap_err();
632        assert_eq!(
633            err.message,
634            "expected the start of a statement (with, for, select, insert, update, delete, or group), \
635             found integer literal '123'"
636        );
637    }
638}