1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! read01 round 340 (V56) — PG has two syntax-error wordings, not dozens.
//!
//! PG 18.4 says exactly `syntax error at or near "<token>"` or
//! `syntax error at end of input`, and the quoted token is the lexeme
//! **as the user wrote it**. SPG wrote its own prose per call site:
//! `expected identifier, got Eof`, `unexpected token From in
//! expression`, and — worst — `expected end of input, got Ident("with")`,
//! which put a Rust Debug rendering of the parser's internal token enum
//! in front of a client.
//!
//! Two defects surfaced while measuring, both silent:
//! * the token index a message pointed at was `pos - 1`, but `advance()`
//! parks on the final Eof, so `SELECT * FROM` named `FROM` where PG
//! says end of input;
//! * `SELECT 1 AS` **parsed clean** and dropped the alias, because the
//! alias parser returned None on a dangling AS and left the error to
//! "the next expectation" — of which there is none at end of input.
//!
//! Every wording below is copied from the PG 18.4 run, not composed.
use spg_engine::Engine;
fn err(e: &mut Engine, sql: &str) -> String {
match e.execute(sql) {
Ok(v) => panic!("{sql}: expected an error, got {v:?}"),
Err(x) => format!("{x}"),
}
}
fn fixture() -> Engine {
let mut e = Engine::new();
e.execute("CREATE TABLE t (a INT)").unwrap();
e
}
/// Truncated input is `at end of input` — never a token name.
#[test]
fn a_truncated_statement_ends_at_end_of_input() {
let mut e = fixture();
for sql in [
"SELECT * FROM",
"SELECT * FROM t WHERE",
"SELECT 1 +",
"INSERT INTO t VALUES",
"UPDATE t SET",
"SELECT * FROM t ORDER",
"SELECT ((1)",
"SELECT * FROM t WHERE a =",
"GRANT",
] {
assert_eq!(
err(&mut e, sql),
"parse: syntax error at end of input",
"for `{sql}`"
);
}
}
/// A token that cannot continue the parse is named — in the source's own
/// spelling, lower-case `frm` included.
#[test]
fn an_offending_token_is_named_as_written() {
let mut e = fixture();
for (sql, tok) in [
("SELECT * frm t", "frm"),
("CREATE TABLE t2 (a int) WITH", "WITH"),
("DELETE t", "t"),
("CREATE TABLE (", "("),
] {
assert_eq!(
err(&mut e, sql),
format!("parse: syntax error at or near \"{tok}\""),
"for `{sql}`"
);
}
}
/// A dangling AS was accepted, and the alias silently vanished.
#[test]
fn a_dangling_as_is_rejected() {
let mut e = fixture();
assert_eq!(
err(&mut e, "SELECT 1 AS"),
"parse: syntax error at end of input"
);
// The well-formed alias still parses, of course.
assert!(e.execute("SELECT 1 AS one").is_ok());
assert!(e.execute("SELECT a AS b FROM t").is_ok());
}
/// Lexer-level failures are PG's wording too — they used to report a byte
/// offset, which is SPG's internal bookkeeping, not an answer.
#[test]
fn an_unterminated_literal_reads_like_pg() {
let mut e = fixture();
assert_eq!(
err(&mut e, "SELECT 'abc"),
"parse: unterminated quoted string at or near \"'abc\"",
);
assert_eq!(
err(&mut e, "SELECT \"abc"),
"parse: unterminated quoted identifier at or near \"\"abc\"",
);
assert_eq!(
err(&mut e, "SELECT 1 /* x"),
"parse: unterminated /* comment at or near \"/* x\"",
);
}
/// The messages PG words itself — its own errors, not its syntax error —
/// must NOT be swept into the generic shape.
#[test]
fn pg_verbatim_messages_are_left_alone() {
let mut e = fixture();
assert_eq!(
err(&mut e, "SELECT * FROM t LIMIT -1"),
"parse: LIMIT must not be negative",
);
assert_eq!(
err(&mut e, "SELECT 'abc'::bigint"),
"eval: type mismatch: invalid input syntax for type bigint: \"abc\"",
);
}